feat: add Arduino Uno R4 WiFi board

- 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
This commit is contained in:
2026-05-29 20:17:47 -07:00
parent 6f9d2912d3
commit 3af958b69f
4 changed files with 729 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
# Uno R4 WiFi Doorbell - Quick Start Guide
## Overview
This is a simplified doorbell notification system for the Arduino Uno R4 WiFi that displays incoming ntfy.sh messages on the built-in 12x8 LED matrix.
## Features
- Connects to WiFi and monitors a ntfy.sh topic
- Displays incoming messages as scrolling text on LED matrix
- Scrolls at 2 columns per second (configurable)
- Auto-silences after 60 seconds of display
- Simple state machine: CONNECTING → IDLE → DISPLAYING → SILENCED
## Setup Instructions
### 1. Configure WiFi Credentials
Edit `boards/uno-r4-wifi/uno-r4-wifi.ino`:
```cpp
#define WIFI_SSID "YOUR_WIFI_SSID"
#define WIFI_PASS "YOUR_WIFI_PASSWORD"
```
### 2. Configure ntfy.sh Topic
Edit `boards/uno-r4-wifi/uno-r4-wifi.ino`:
```cpp
#define NTFY_TOPIC "YOUR_TOPIC_HERE"
```
Create a free topic at https://ntfy.sh/ (e.g., "my-doorbell-12345")
### 3. Upload the Sketch
```bash
arduino-cli compile --fqbn arduino:renesas_uno:unor4wifi boards/uno-r4-wifi/uno-r4-wifi.ino
arduino-cli upload --fqbn arduino:renesas_uno:unor4wifi -p /dev/ttyACM0 boards/uno-r4-wifi/uno-r4-wifi.ino
```
### 4. Test the System
Open a terminal and run the test script:
```bash
chmod +x boards/uno-r4-wifi/test_messages.sh
./boards/uno-r4-wifi/test_messages.sh
```
Or manually send a message:
```bash
curl -X POST https://ntfy.sh/YOUR_TOPIC_HERE -d "Hello from ntfy!"
```
## How It Works
### State Machine
1. **CONNECTING_WIFI** - Attempts to connect to WiFi
2. **IDLE** - Polls ntfy.sh every 5 seconds for new messages
3. **DISPLAYING_MESSAGE** - Shows scrolling text for 60 seconds
4. **SILENCED** - Clears matrix for 5 seconds before returning to IDLE
### LED Matrix Display
- Text scrolls from right to left across the 12x8 matrix
- Scroll rate: 2 columns per second (500ms per column)
- Characters are 5 pixels wide with 1 pixel spacing
- Only alphanumeric characters (A-Z, 0-9) and spaces are supported
### ntfy.sh Integration
- Uses HTTP GET with polling (`/json?poll=1`)
- Tracks last message ID to avoid duplicates
- Parses JSON response for message body
## Pinout Reference (Uno R4 WiFi)
- **D0-D13**: Standard digital pins
- **A0-A5**: Analog inputs
- **D26/D27**: Qwiic (I2C) connector (SCL/SDA)
- **LED Matrix**: Built-in 12x8 matrix (pins handled internally)
## Customization
### Adjust Timing
Edit constants in `uno-r4-wifi.ino`:
```cpp
#define POLL_INTERVAL_MS 5000 // How often to check for messages
#define DISPLAY_DURATION_MS 60000 // How long to display each message
#define SCROLL_SPEED_MS 500 // Scroll speed (lower = faster)
#define SILENCE_DURATION_MS 5000 // Time between messages
```
### Add More Characters
The font array in `drawCharToFrame()` can be extended to support additional characters.
## Troubleshooting
### WiFi Won't Connect
- Check WiFi credentials
- Ensure 2.4GHz network (Uno R4 WiFi doesn't support 5GHz)
- Verify WiFi signal strength
### LED Matrix Not Showing Text
- Ensure `matrix.begin()` is called in setup()
- Check that `matrix.loadFrame()` is called with valid data
- Verify text contains only supported characters
### No Messages Received
- Check ntfy.sh topic name matches exactly
- Ensure the topic is publicly accessible
- Test with curl command manually
- Check serial monitor for error messages
## Next Steps
- Add support for more characters in the font
- Implement different message types (Alert, Silence, etc.)
- Add color coding with different LED patterns
- Integrate with the main doorbell codebase
+4
View File
@@ -0,0 +1,4 @@
FQBN=arduino:renesas_uno:unor4wifi
PORT=/dev/ttyACM0
LIBS=""
OPTS="-DDEBUG_MODE"
+51
View File
@@ -0,0 +1,51 @@
#!/bin/bash
# Test script for Uno R4 WiFi doorbell
# Sends test messages to ntfy.sh
# Update this with your actual topic
NTFY_TOPIC="ALERT_klubhaus_topic_test"
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${YELLOW}Uno R4 WiFi Doorbell Test Script${NC}"
echo "======================================"
echo -e "Sending test messages to: https://ntfy.sh/$NTFY_TOPIC"
echo ""
# Send different test messages
echo "1. Sending simple alert..."
curl -s -X POST "https://ntfy.sh/$NTFY_TOPIC" \
-d "Doorbell pressed!" \
-H "Title: ALERT" \
-H "Priority: high"
sleep 2
echo -e "\n2. Sending longer message..."
curl -s -X POST "https://ntfy.sh/$NTFY_TOPIC" \
-d "Front door visitor detected at main entrance" \
-H "Title: ALERT" \
-H "Priority: high"
sleep 2
echo -e "\n3. Sending test with special characters..."
curl -s -X POST "https://ntfy.sh/$NTFY_TOPIC" \
-d "Package delivered! #12345" \
-H "Title: ALERT" \
-H "Priority: high"
sleep 2
echo -e "\n4. Sending numeric message..."
curl -s -X POST "https://ntfy.sh/$NTFY_TOPIC" \
-d "Temperature: 72F" \
-H "Title: ALERT" \
-H "Priority: high"
echo -e "\n${GREEN}Done! Check the LED matrix on your Uno R4 WiFi.${NC}"
+563
View File
@@ -0,0 +1,563 @@
//
// 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);
}