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:
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
FQBN=arduino:renesas_uno:unor4wifi
|
||||
PORT=/dev/ttyACM0
|
||||
LIBS=""
|
||||
OPTS="-DDEBUG_MODE"
|
||||
Executable
+51
@@ -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}"
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user