3af958b69f
- LED matrix doorbell display with scrolling text - Non-blocking HTTP poll state machine for ntfy.sh - Ticker tape pressure gauge (12 cols, 3-min window, PWM decay) - Unix timestamp message filtering (readyTime after NTP sync) - BUILTIN LED sine-wave brightness heartbeat - Pending: remove backup file before merge
564 lines
16 KiB
Arduino
564 lines
16 KiB
Arduino
//
|
|
// Klubhaus Doorbell - Uno R4 WiFi Edition
|
|
// Monitors ntfy.sh alert topic and displays messages on LED matrix
|
|
//
|
|
|
|
#include "Arduino_LED_Matrix.h"
|
|
#include "WiFiS3.h"
|
|
#include "ArduinoJson.h"
|
|
#include "RTC.h" // Add RTC library for time synchronization
|
|
|
|
// WiFi credentials
|
|
#define WIFI_SSID "iot-2GHz"
|
|
#define WIFI_PASS "lesson-greater"
|
|
|
|
// ntfy.sh topic
|
|
#define NTFY_TOPIC "ALERT_klubhaus_topic_test"
|
|
|
|
// Timing constants
|
|
#define POLL_INTERVAL_MS 5000 // Poll ntfy.sh every 5 seconds
|
|
#define DISPLAY_DURATION_MS 60000 // Show message for 60 seconds
|
|
#define SCROLL_SPEED_MS 100 // Scroll speed (lower = faster) - 4 columns/sec
|
|
#define SILENCE_DURATION_MS 5000 // Stay silenced for 5 seconds
|
|
#define GAUGE_MAX_AGE_MS 180000 // 3-minute window for gauge
|
|
#define GAUGE_BUCKET_SEC 15 // 15 seconds per column
|
|
#define MAX_GAUGE_ENTRIES 12 // Max message timestamps
|
|
|
|
long gaugeTimestamps[MAX_GAUGE_ENTRIES] = {0};
|
|
int gaugeCount = 0;
|
|
|
|
// LED matrix dimensions
|
|
#define MATRIX_COLS 12
|
|
#define MATRIX_ROWS 8
|
|
|
|
// Global objects
|
|
ArduinoLEDMatrix matrix;
|
|
WiFiClient client;
|
|
|
|
// State machine
|
|
enum AppState {
|
|
STATE_IDLE,
|
|
STATE_DISPLAYING_MESSAGE
|
|
};
|
|
AppState currentState = STATE_IDLE;
|
|
|
|
// Message state
|
|
String currentMessage = "";
|
|
String lastDisplayedMessage = ""; // Track last displayed message to avoid duplicates
|
|
String lastMessageId = ""; // Track last message ID for filtering
|
|
unsigned long messageStartTime = 0;
|
|
unsigned long lastScrollTime = 0;
|
|
unsigned long lastPollTime = 0;
|
|
unsigned long bootTime = 0;
|
|
unsigned long bootDelayEndTime = 0; // When to start processing messages
|
|
unsigned long lastDisplayedMessageTime = 0; // Unix timestamp of last displayed message
|
|
long currentMessageTime = 0; // Unix timestamp of current message being displayed
|
|
|
|
// Non-blocking poll state machine
|
|
enum PollState { POLL_IDLE, POLL_WAIT, POLL_CONNECT, POLL_SEND, POLL_READ, POLL_PARSE };
|
|
PollState pollState = POLL_IDLE;
|
|
unsigned long pollStepTime = 0;
|
|
String pollResponse = "";
|
|
long readyTime = 0; // Unix timestamp when device became ready (after NTP sync)
|
|
int queuedMessageCount = 0;
|
|
bool rtcSynced = false; // Track if RTC has been synced with NTP
|
|
|
|
// Scrolling state
|
|
int scrollOffset = 0;
|
|
int messageLength = 0;
|
|
|
|
// Frame buffer - 8 rows x 12 columns
|
|
uint8_t frameBuffer[MATRIX_ROWS][MATRIX_COLS] = {0};
|
|
|
|
int getBracketPeriod(long ageSec) {
|
|
if (ageSec < 15) return 150;
|
|
if (ageSec < 30) return 200;
|
|
if (ageSec < 45) return 250;
|
|
if (ageSec < 60) return 350;
|
|
if (ageSec < 75) return 500;
|
|
if (ageSec < 90) return 700;
|
|
if (ageSec < 105) return 1000;
|
|
if (ageSec < 120) return 1400;
|
|
if (ageSec < 135) return 1800;
|
|
if (ageSec < 150) return 2200;
|
|
if (ageSec < 165) return 2600;
|
|
return 3000;
|
|
}
|
|
|
|
int getBracketDuty(long ageSec) {
|
|
if (ageSec < 15) return 55;
|
|
if (ageSec < 30) return 48;
|
|
if (ageSec < 45) return 40;
|
|
if (ageSec < 60) return 35;
|
|
if (ageSec < 75) return 30;
|
|
if (ageSec < 90) return 25;
|
|
if (ageSec < 105) return 20;
|
|
if (ageSec < 120) return 16;
|
|
if (ageSec < 135) return 12;
|
|
if (ageSec < 150) return 10;
|
|
if (ageSec < 165) return 8;
|
|
return 6;
|
|
}
|
|
|
|
void gaugeAddMessage(long unixTime) {
|
|
if (gaugeCount < MAX_GAUGE_ENTRIES) {
|
|
gaugeTimestamps[gaugeCount++] = unixTime;
|
|
} else {
|
|
for (int i = 0; i < MAX_GAUGE_ENTRIES - 1; i++) {
|
|
gaugeTimestamps[i] = gaugeTimestamps[i + 1];
|
|
}
|
|
gaugeTimestamps[MAX_GAUGE_ENTRIES - 1] = unixTime;
|
|
}
|
|
}
|
|
|
|
void gaugePruneOld(long nowUnix) {
|
|
int w = 0;
|
|
for (int i = 0; i < gaugeCount; i++) {
|
|
if (nowUnix - gaugeTimestamps[i] <= GAUGE_MAX_AGE_MS / 1000) {
|
|
gaugeTimestamps[w++] = gaugeTimestamps[i];
|
|
}
|
|
}
|
|
gaugeCount = w;
|
|
}
|
|
|
|
void renderGauge(long nowUnix) {
|
|
unsigned long ms = millis();
|
|
for (int col = 0; col < MATRIX_COLS; col++) {
|
|
int bucketMin = col * GAUGE_BUCKET_SEC;
|
|
int bucketMax = bucketMin + GAUGE_BUCKET_SEC;
|
|
|
|
int messagesInBucket = 0;
|
|
long youngestAge = GAUGE_MAX_AGE_MS / 1000 + 1;
|
|
|
|
for (int i = 0; i < gaugeCount; i++) {
|
|
long age = nowUnix - gaugeTimestamps[i];
|
|
if (age >= bucketMin && age < bucketMax) {
|
|
messagesInBucket++;
|
|
if (age < youngestAge) youngestAge = age;
|
|
}
|
|
}
|
|
|
|
if (messagesInBucket == 0) {
|
|
frameBuffer[MATRIX_ROWS - 1][col] = 0;
|
|
continue;
|
|
}
|
|
|
|
int periodMs = getBracketPeriod(youngestAge);
|
|
int dutyPct = getBracketDuty(youngestAge);
|
|
|
|
int phase = ms % periodMs;
|
|
int onThreshold = periodMs * dutyPct / 100;
|
|
frameBuffer[MATRIX_ROWS - 1][col] = (phase < onThreshold) ? 1 : 0;
|
|
}
|
|
}
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
delay(1000);
|
|
Serial.println("\n=== Klubhaus Doorbell - Uno R4 WiFi ===");
|
|
Serial.print("Build token: ");
|
|
Serial.println(BUILD_TOKEN);
|
|
Serial.println("Starting up...");
|
|
|
|
// Initialize RTC
|
|
RTC.begin();
|
|
Serial.println("RTC initialized");
|
|
|
|
bootTime = millis();
|
|
bootDelayEndTime = bootTime + 10000; // Wait 10 seconds after boot before processing
|
|
Serial.print("Boot time: ");
|
|
Serial.print(bootTime);
|
|
Serial.print(", will start processing at: ");
|
|
Serial.println(bootDelayEndTime);
|
|
|
|
Serial.println("Initializing LED matrix...");
|
|
matrix.begin();
|
|
delay(100);
|
|
matrix.clear();
|
|
Serial.println("LED matrix initialized");
|
|
|
|
Serial.println("Displaying OK...");
|
|
displayStaticText("OK");
|
|
delay(500); // Display OK for 500ms
|
|
matrix.clear();
|
|
|
|
Serial.println("Starting WiFi connection...");
|
|
connectToWiFi();
|
|
|
|
// Sync RTC with NTP after WiFi connection
|
|
syncRTCWithNTP();
|
|
}
|
|
|
|
void loop() {
|
|
unsigned long now = millis();
|
|
|
|
// Heartbeat LED
|
|
static unsigned long lastHeartbeat = 0;
|
|
static const unsigned long FADE_PERIOD_MS = 10000;
|
|
static const int MIN_BRIGHTNESS = 51;
|
|
static const int MAX_BRIGHTNESS = 204;
|
|
if (now - lastHeartbeat >= 50) {
|
|
float phase = (float)(now % FADE_PERIOD_MS) / (float)FADE_PERIOD_MS * 2.0 * PI;
|
|
analogWrite(LED_BUILTIN, MIN_BRIGHTNESS + (int)((MAX_BRIGHTNESS - MIN_BRIGHTNESS) / 2.0 * (1.0 + sin(phase))));
|
|
lastHeartbeat = now;
|
|
}
|
|
|
|
// One RTC read per loop
|
|
RTCTime ct;
|
|
RTC.getTime(ct);
|
|
long nowUnix = ct.getUnixTime();
|
|
|
|
// Non-blocking poll — one step per loop
|
|
if (rtcSynced && now >= bootDelayEndTime) {
|
|
pollNtfy(nowUnix);
|
|
}
|
|
|
|
// Clear frame, then always render gauge on row 7
|
|
memset(frameBuffer, 0, sizeof(frameBuffer));
|
|
gaugePruneOld(nowUnix);
|
|
renderGauge(nowUnix);
|
|
|
|
switch (currentState) {
|
|
case STATE_IDLE:
|
|
if (currentMessage.length() > 0) {
|
|
Serial.print("[DISPLAY] ");
|
|
Serial.println(currentMessage);
|
|
currentState = STATE_DISPLAYING_MESSAGE;
|
|
messageStartTime = now;
|
|
lastScrollTime = now;
|
|
scrollOffset = -MATRIX_COLS;
|
|
messageLength = currentMessage.length();
|
|
}
|
|
break;
|
|
|
|
case STATE_DISPLAYING_MESSAGE:
|
|
// Always draw text at current scroll position (not just on tick)
|
|
drawScrollingText();
|
|
|
|
// Advance scroll on timer tick
|
|
if (now - lastScrollTime >= SCROLL_SPEED_MS) {
|
|
scrollOffset += 1;
|
|
if (scrollOffset > messageLength * 6 + MATRIX_COLS) {
|
|
scrollOffset = -MATRIX_COLS;
|
|
}
|
|
lastScrollTime = now;
|
|
}
|
|
|
|
if (now - messageStartTime >= DISPLAY_DURATION_MS) {
|
|
lastDisplayedMessage = currentMessage;
|
|
lastDisplayedMessageTime = currentMessageTime;
|
|
currentState = STATE_IDLE;
|
|
currentMessage = "";
|
|
scrollOffset = 0;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Non-blocking poll — start cycle on timer, advance one step per loop
|
|
static unsigned long lastPollStart = 0;
|
|
if (pollState == POLL_IDLE && now - lastPollStart >= POLL_INTERVAL_MS) {
|
|
if (rtcSynced && now >= bootDelayEndTime && WiFi.status() == WL_CONNECTED) {
|
|
pollState = POLL_CONNECT;
|
|
lastPollStart = now;
|
|
}
|
|
}
|
|
if (pollState != POLL_IDLE) {
|
|
pollNtfy(nowUnix);
|
|
}
|
|
|
|
matrix.renderBitmap(frameBuffer, MATRIX_ROWS, MATRIX_COLS);
|
|
}
|
|
|
|
void connectToWiFi() {
|
|
Serial.print("Connecting to WiFi: ");
|
|
Serial.println(WIFI_SSID);
|
|
|
|
if (WiFi.status() == WL_NO_MODULE) {
|
|
Serial.println("ERROR: Communication with WiFi module failed!");
|
|
displayStaticText("ERR");
|
|
while (true);
|
|
}
|
|
|
|
String fv = WiFi.firmwareVersion();
|
|
Serial.print("WiFi firmware version: ");
|
|
Serial.println(fv);
|
|
|
|
Serial.print("Attempting WiFi connection...");
|
|
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
|
|
|
int attempts = 0;
|
|
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
|
|
Serial.print(".");
|
|
delay(1000);
|
|
attempts++;
|
|
}
|
|
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
Serial.println("\nERROR: Failed to connect to WiFi");
|
|
displayStaticText("WIFI ERR");
|
|
while (true);
|
|
}
|
|
|
|
Serial.println("\nWiFi connected!");
|
|
Serial.print("IP address: ");
|
|
Serial.println(WiFi.localIP());
|
|
}
|
|
|
|
void syncRTCWithNTP() {
|
|
Serial.println("Syncing RTC with NTP...");
|
|
|
|
unsigned long epochTime = WiFi.getTime();
|
|
|
|
if (epochTime > 0) {
|
|
// Set timezone to UTC (no offset)
|
|
RTCTime timeToSet = RTCTime(epochTime);
|
|
RTC.setTime(timeToSet);
|
|
|
|
// Get current time to verify
|
|
RTCTime currentTime;
|
|
RTC.getTime(currentTime);
|
|
Serial.print("RTC synced to: ");
|
|
Serial.println(String(currentTime));
|
|
|
|
rtcSynced = true;
|
|
|
|
readyTime = epochTime; // Reject messages older than this boot
|
|
Serial.print("Ready time: ");
|
|
Serial.println((unsigned long)readyTime);
|
|
} else {
|
|
Serial.println("ERROR: Failed to get NTP time");
|
|
Serial.println("Make sure WiFi firmware version is at least 0.5.0");
|
|
}
|
|
}
|
|
|
|
void pollNtfy(long nowUnix) {
|
|
unsigned long now = millis();
|
|
|
|
switch (pollState) {
|
|
case POLL_CONNECT:
|
|
if (client.connect("ntfy.sh", 80)) {
|
|
pollState = POLL_SEND;
|
|
} else {
|
|
pollState = POLL_IDLE;
|
|
}
|
|
break;
|
|
|
|
case POLL_SEND: {
|
|
String url;
|
|
if (lastMessageId.length() > 0) {
|
|
url = "/" + String(NTFY_TOPIC) + "/json?poll=1&since=" + lastMessageId;
|
|
} else {
|
|
url = "/" + String(NTFY_TOPIC) + "/json?poll=1&since=latest";
|
|
}
|
|
client.print(String("GET ") + url + " HTTP/1.1\r\n" +
|
|
"Host: ntfy.sh\r\n" +
|
|
"Connection: close\r\n\r\n");
|
|
pollStepTime = now;
|
|
pollResponse = "";
|
|
pollState = POLL_READ;
|
|
break;
|
|
}
|
|
|
|
case POLL_READ: {
|
|
// Non-blocking read — only consume what's available, max 20ms per call
|
|
unsigned long t = millis();
|
|
while (client.available() > 0 && (millis() - t) < 20) {
|
|
pollResponse += (char)client.read();
|
|
}
|
|
// Done when disconnected and we have content, or timeout
|
|
if (!client.connected() || (pollResponse.length() > 0 && now - pollStepTime > 3000)) {
|
|
client.stop();
|
|
pollState = POLL_PARSE;
|
|
} else if (now - pollStepTime > 8000) {
|
|
client.stop();
|
|
pollState = POLL_IDLE;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case POLL_PARSE: {
|
|
int js = pollResponse.indexOf("{");
|
|
if (js >= 0) {
|
|
StaticJsonDocument<1024> doc;
|
|
if (!deserializeJson(doc, pollResponse.substring(js))) {
|
|
processMessage(doc, nowUnix);
|
|
}
|
|
}
|
|
pollState = POLL_IDLE;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void processMessage(JsonDocument& doc, long nowUnix) {
|
|
const char* message = doc["message"];
|
|
const char* id = doc["id"];
|
|
long msgTime = doc["time"];
|
|
|
|
if (!message || strlen(message) == 0 || msgTime <= 0) return;
|
|
|
|
String msgStr = String(message);
|
|
|
|
if (msgStr == lastDisplayedMessage) {
|
|
if (id) lastMessageId = String(id);
|
|
return;
|
|
}
|
|
if (lastDisplayedMessageTime > 0 && msgTime <= lastDisplayedMessageTime) {
|
|
if (id) lastMessageId = String(id);
|
|
return;
|
|
}
|
|
if (readyTime > 0 && msgTime < readyTime) {
|
|
if (id) lastMessageId = String(id);
|
|
return;
|
|
}
|
|
if (nowUnix - msgTime > 3600) {
|
|
if (id) lastMessageId = String(id);
|
|
return;
|
|
}
|
|
|
|
Serial.print("[ACCEPTED] ");
|
|
Serial.println(msgStr);
|
|
|
|
gaugePruneOld(nowUnix);
|
|
gaugeAddMessage(msgTime);
|
|
|
|
if (currentState == STATE_IDLE) {
|
|
currentMessage = msgStr;
|
|
currentMessageTime = msgTime;
|
|
}
|
|
if (id) lastMessageId = String(id);
|
|
}
|
|
|
|
void drawScrollingText() {
|
|
int charWidth = 5;
|
|
int charSpacing = 1;
|
|
|
|
for (int i = 0; i < messageLength; i++) {
|
|
char c = currentMessage[i];
|
|
int charX = i * (charWidth + charSpacing) - scrollOffset;
|
|
if (charX > -charWidth && charX < MATRIX_COLS) {
|
|
drawCharToFrame(c, charX);
|
|
}
|
|
}
|
|
}
|
|
|
|
void drawCharToFrame(char c, int x) {
|
|
static const uint8_t font[][5] = {
|
|
// Uppercase A-Z (indices 0-25)
|
|
{0x7F, 0x41, 0x41, 0x41, 0x7F}, // 'A'
|
|
{0x7F, 0x49, 0x49, 0x49, 0x36}, // 'B'
|
|
{0x3E, 0x41, 0x41, 0x41, 0x22}, // 'C'
|
|
{0x7F, 0x41, 0x41, 0x22, 0x1C}, // 'D'
|
|
{0x7F, 0x49, 0x49, 0x49, 0x41}, // 'E'
|
|
{0x7F, 0x09, 0x09, 0x09, 0x01}, // 'F'
|
|
{0x3E, 0x41, 0x49, 0x49, 0x7A}, // 'G'
|
|
{0x7F, 0x08, 0x08, 0x08, 0x7F}, // 'H'
|
|
{0x00, 0x41, 0x7F, 0x41, 0x00}, // 'I'
|
|
{0x20, 0x40, 0x41, 0x3F, 0x00}, // 'J'
|
|
{0x7F, 0x08, 0x14, 0x22, 0x41}, // 'K'
|
|
{0x7F, 0x40, 0x40, 0x40, 0x40}, // 'L'
|
|
{0x7F, 0x02, 0x0C, 0x02, 0x7F}, // 'M'
|
|
{0x7F, 0x04, 0x08, 0x10, 0x7F}, // 'N'
|
|
{0x3E, 0x41, 0x41, 0x41, 0x3E}, // 'O'
|
|
{0x7F, 0x09, 0x09, 0x09, 0x06}, // 'P'
|
|
{0x3E, 0x41, 0x51, 0x21, 0x5E}, // 'Q'
|
|
{0x7F, 0x09, 0x19, 0x29, 0x46}, // 'R'
|
|
{0x46, 0x49, 0x49, 0x49, 0x31}, // 'S'
|
|
{0x01, 0x01, 0x7F, 0x01, 0x01}, // 'T'
|
|
{0x3F, 0x40, 0x40, 0x40, 0x3F}, // 'U'
|
|
{0x1F, 0x20, 0x40, 0x20, 0x1F}, // 'V'
|
|
{0x7F, 0x20, 0x18, 0x20, 0x7F}, // 'W'
|
|
{0x63, 0x14, 0x08, 0x14, 0x63}, // 'X'
|
|
{0x07, 0x08, 0x70, 0x08, 0x07}, // 'Y'
|
|
{0x61, 0x51, 0x49, 0x45, 0x43}, // 'Z'
|
|
// Lowercase a-z (indices 26-51)
|
|
{0x7F, 0x09, 0x09, 0x09, 0x7F}, // 'a'
|
|
{0x7F, 0x49, 0x49, 0x49, 0x36}, // 'b'
|
|
{0x3E, 0x41, 0x41, 0x41, 0x22}, // 'c'
|
|
{0x7F, 0x41, 0x41, 0x22, 0x1C}, // 'd'
|
|
{0x7F, 0x49, 0x49, 0x49, 0x41}, // 'e'
|
|
{0x7F, 0x09, 0x09, 0x09, 0x01}, // 'f'
|
|
{0x3E, 0x41, 0x49, 0x49, 0x7A}, // 'g'
|
|
{0x7F, 0x08, 0x08, 0x08, 0x7F}, // 'h'
|
|
{0x00, 0x41, 0x7F, 0x41, 0x00}, // 'i'
|
|
{0x20, 0x40, 0x41, 0x3F, 0x00}, // 'j'
|
|
{0x7F, 0x08, 0x14, 0x22, 0x41}, // 'k'
|
|
{0x7F, 0x40, 0x40, 0x40, 0x40}, // 'l'
|
|
{0x7F, 0x02, 0x0C, 0x02, 0x7F}, // 'm'
|
|
{0x7F, 0x04, 0x08, 0x10, 0x7F}, // 'n'
|
|
{0x3E, 0x41, 0x41, 0x41, 0x3E}, // 'o'
|
|
{0x7F, 0x09, 0x09, 0x09, 0x06}, // 'p'
|
|
{0x3E, 0x41, 0x51, 0x21, 0x5E}, // 'q'
|
|
{0x7F, 0x09, 0x19, 0x29, 0x46}, // 'r'
|
|
{0x46, 0x49, 0x49, 0x49, 0x31}, // 's'
|
|
{0x01, 0x01, 0x7F, 0x01, 0x01}, // 't'
|
|
{0x3F, 0x40, 0x40, 0x40, 0x3F}, // 'u'
|
|
{0x1F, 0x20, 0x40, 0x20, 0x1F}, // 'v'
|
|
{0x7F, 0x20, 0x18, 0x20, 0x7F}, // 'w'
|
|
{0x63, 0x14, 0x08, 0x14, 0x63}, // 'x'
|
|
{0x07, 0x08, 0x70, 0x08, 0x07}, // 'y'
|
|
{0x61, 0x51, 0x49, 0x45, 0x43}, // 'z'
|
|
// Numbers and space (indices 52-62)
|
|
{0x3E, 0x51, 0x49, 0x45, 0x3E}, // '0'
|
|
{0x00, 0x42, 0x7F, 0x40, 0x00}, // '1'
|
|
{0x42, 0x61, 0x51, 0x49, 0x46}, // '2'
|
|
{0x21, 0x41, 0x45, 0x4B, 0x31}, // '3'
|
|
{0x18, 0x14, 0x12, 0x7F, 0x10}, // '4'
|
|
{0x27, 0x45, 0x45, 0x45, 0x39}, // '5'
|
|
{0x3C, 0x4A, 0x49, 0x49, 0x30}, // '6'
|
|
{0x01, 0x71, 0x09, 0x05, 0x03}, // '7'
|
|
{0x36, 0x49, 0x49, 0x49, 0x36}, // '8'
|
|
{0x06, 0x49, 0x49, 0x29, 0x1E}, // '9'
|
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // ' ' (space)
|
|
};
|
|
|
|
int fontIndex = -1;
|
|
|
|
if (c >= 'A' && c <= 'Z') {
|
|
fontIndex = c - 'A';
|
|
}
|
|
else if (c >= 'a' && c <= 'z') {
|
|
fontIndex = 26 + (c - 'a');
|
|
}
|
|
else if (c >= '0' && c <= '9') {
|
|
fontIndex = 52 + (c - '0');
|
|
}
|
|
else if (c == ' ') {
|
|
fontIndex = 62;
|
|
}
|
|
else {
|
|
return;
|
|
}
|
|
|
|
if (fontIndex >= 0) {
|
|
for (int col = 0; col < 5; col++) {
|
|
uint8_t colData = font[fontIndex][col];
|
|
int drawX = x + col;
|
|
|
|
if (drawX >= 0 && drawX < MATRIX_COLS) {
|
|
for (int row = 0; row < MATRIX_ROWS; row++) {
|
|
if (colData & (1 << row)) {
|
|
frameBuffer[row][drawX] = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void displayStaticText(String text) {
|
|
memset(frameBuffer, 0, sizeof(frameBuffer));
|
|
|
|
int textWidth = text.length() * 6;
|
|
int startX = max(0, (MATRIX_COLS - textWidth) / 2);
|
|
|
|
for (int i = 0; i < text.length(); i++) {
|
|
drawCharToFrame(text[i], startX + i * 6);
|
|
}
|
|
|
|
matrix.renderBitmap(frameBuffer, MATRIX_ROWS, MATRIX_COLS);
|
|
}
|