Add SignalRGB control via WLED emulation

The clock answers like a WLED device (mDNS _wled._tcp, /json/info,
/json/state, realtime UDP on 21324 and DDP on 4048), so SignalRGB can
drive the 4 backlight LEDs, one per tube, LED 0 = left tube. Frames are
shown at most 30 times a second (WLED_MAX_FPS); the normal effect
returns 2.5 s after they stop, and the alarm flash still wins.

Three new settings on the RGB page: enable (also a Home Assistant
switch), stay dark while the tubes are off, and scale by the RGB
brightness. They are stored in EEPROM after prm with their own magic
value, so the update does not factory-reset the clock.

page.js and site.css are now sent with no-cache: browsers kept them
for a year and ran the old script after a web UI update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-25 23:32:25 +01:00
1 parent c4e0c29791
commit ae87849e75
9 files changed
+446 -20

No files matched your search

@@ -270,6 +270,7 @@ extern char tubeDriver[]; //human-readable driver name string, shown on the web
char webName[] = WEBNAME;
AsyncWebServer server(80);
#define CACHE_MAX_AGE "max-age=31536000" //maximum is: 31536000
#define CACHE_REVALIDATE "no-cache" //page.js and site.css change with web UI updates (uploadfs): don't let browsers keep an old copy
#ifdef USE_WIFIMANAGER
#include <ESPAsyncWiFiManager.h>
@@ -342,6 +343,7 @@ bool colonBlinkState = false;
boolean radarON = true;
boolean mqttRadarON = true;
volatile boolean tubesOn = true; //Tubes switch from Home Assistant (runtime only, not saved); honoured by nixie595.ino
boolean wledLive = false; //SignalRGB/WLED frames are arriving and drive the backlight (wled.ino)
unsigned long radarLastOn = 0;
boolean makeFirmwareUpdate = false;
boolean makeCathodeProtect = false;
@@ -441,7 +443,19 @@ struct {
int magic = MAGIC_VALUE; //magic value, to check EEPROM version when starting the clock - see loadEEPROM()/factoryReset() and the MAGIC_VALUE comment above
} prm;
#define EEPROM_SIZE sizeof(prm) //EEPROM.begin() is sized exactly to the struct - no extra reserved space for future growth, so any struct change is effectively a breaking EEPROM-layout change (mitigated only by the magic-number factory-reset above)
// Settings added by this port, stored in EEPROM right after `prm` with a magic value of their own.
// Adding them to `prm` would change its layout, and the MAGIC_VALUE check would then factory-reset
// the clock (WiFi settings included). If the magic value doesn't match (first boot after the update),
// loadEEPROM() sets these defaults and leaves `prm` alone.
#define MAGIC_VALUE_EXT 0x5701
struct {
boolean wledEnable = false; //SignalRGB/WLED realtime control of the backlight (wled.ino)
boolean wledDarkWithTubes = true; //WLED frames stay dark while the tubes/backlight are off
boolean wledUseBrightness = true; //WLED frames are scaled by the RGB brightness (and auto dim)
uint16_t magic = MAGIC_VALUE_EXT;
} prmExt;
#define EEPROM_SIZE (sizeof(prm) + sizeof(prmExt)) //no extra reserved space for future growth: a change to either struct needs its own magic value bumped
//-------------------------------------------------------------------------------------------------------
@@ -646,6 +660,7 @@ void Fdelay(unsigned long d) {
#ifdef USE_ARDUINO_OTA
DIAG("F:ota", ArduinoOTA.handle()); //also during long animations/startup waits
#endif
DIAG("F:wled", wledHandle());
DIAG("F:yield", yield());
}
}
@@ -1187,8 +1202,8 @@ void startServer() {
DPRINTLN("Webserver: /page.js");
if (SPIFFS.exists("/page.js")) {
AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/page.js", "text/js");
response->addHeader("Cache-Control", CACHE_MAX_AGE);
AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/page.js", "text/js");
response->addHeader("Cache-Control", CACHE_REVALIDATE);
request->send(response);
}
else {
@@ -1232,8 +1247,8 @@ void startServer() {
server.on("/site.css", HTTP_GET, [](AsyncWebServerRequest * request) {
DPRINTLN("Webserver: /site.css");
if (SPIFFS.exists("/site.css")) {
AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/site.css", "text/css");
response->addHeader("Cache-Control", CACHE_MAX_AGE);
AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/site.css", "text/css");
response->addHeader("Cache-Control", CACHE_REVALIDATE);
request->send(response);
}
else {
@@ -1273,6 +1288,7 @@ void startServer() {
server.on("/getConfiguration", HTTP_GET, handleSendConfig) WEB_AUTH;
server.on("/getCurrentInfos", HTTP_GET, handleSendCurrentInfos);
server.on("/getClockDetails", HTTP_GET, handleSendClockDetails) WEB_AUTH;
wledRoutes(); //WLED JSON API for SignalRGB, without password (wled.ino)
server.onNotFound(handleNotFound);
server.begin();
@@ -1457,6 +1473,17 @@ void handleConfigChanged(AsyncWebServerRequest *request) {
else if (key == "rgbMinBrightness") {
c_MinBrightness = value.toInt();
}
#ifdef USE_WLED_REALTIME
else if (key == "wledEnable") { //wledHandle() starts/stops the listeners
prmExt.wledEnable = (value == "true");
}
else if (key == "wledDarkWithTubes") {
prmExt.wledDarkWithTubes = (value == "true");
}
else if (key == "wledUseBrightness") {
prmExt.wledUseBrightness = (value == "true");
}
#endif
else if (key == "wifiSsid") {
value.toCharArray(prm.wifiSsid,sizeof(prm.wifiSsid));
}
@@ -1683,6 +1710,11 @@ if (useTemp > 1)
doc["rgbSpeed"] = prm.rgbSpeed; // 1..255
doc["rgbDir"] = prm.rgbDir; // 0 = right direction, 1 = left direction
doc["rgbMinBrightness"] = c_MinBrightness; //minimum brightness for range check!!
#ifdef USE_WLED_REALTIME //the web page hides these switches when they are missing
doc["wledEnable"] = prmExt.wledEnable;
doc["wledDarkWithTubes"] = prmExt.wledDarkWithTubes;
doc["wledUseBrightness"] = prmExt.wledUseBrightness;
#endif
doc["wifiMode"] = prm.wifiMode;
doc["wifiSsid"] = prm.wifiSsid;
doc["wifiPsw"] = prm.wifiPsw;
@@ -1809,6 +1841,9 @@ if (useTemp > 1)
doc["rssi"] = WiFi.RSSI();
doc["uptime"] = millis() / 1000;
doc["heap"] = ESP.getFreeHeap();
#ifdef USE_WLED_REALTIME
doc["wledLive"] = wledLive; //SignalRGB is sending frames
#endif
#ifdef LOOP_DIAG
doc["stalls"] = diagReport();
#endif
@@ -2004,6 +2039,7 @@ void setup() {
startMDNS();
startServer();
startArduinoOTA();
wledSetup(); //after startArduinoOTA(): uses its mDNS responder
#ifdef USE_MQTT
setupMqtt();
#endif
@@ -2140,6 +2176,16 @@ void loadEEPROM() {
EEPROM.get(EEPROM_addr, prm);
DPRINT(" version:"); DPRINTLN(prm.magic);
EEPROM.get(EEPROM_addr + sizeof(prm), prmExt);
if (prmExt.magic != MAGIC_VALUE_EXT) resetPrmExt();
}
// Defaults for prmExt (not saved here: the caller saves when it needs to).
void resetPrmExt() {
prmExt.wledEnable = false;
prmExt.wledDarkWithTubes = true;
prmExt.wledUseBrightness = true;
prmExt.magic = MAGIC_VALUE_EXT;
}
// Raw single-block write of the whole `prm` struct back to EEPROM (address 0) - called after every
@@ -2148,6 +2194,7 @@ void loadEEPROM() {
// full-struct flash write, not just the changed bytes).
void saveEEPROM() {
EEPROM.put(EEPROM_addr, prm);
EEPROM.put(EEPROM_addr + sizeof(prm), prmExt);
EEPROM.commit();
DPRINTLN("Settings saved to EEPROM!");
}
@@ -2228,6 +2275,7 @@ void factoryReset() {
prm.corrH0 = 0;
prm.corrH1 = 0;
prm.magic = MAGIC_VALUE; //magic value to check the EEPROM version
resetPrmExt();
saveEEPROM();
calcTime();
}
@@ -3345,6 +3393,7 @@ void loop(void) {
#ifdef USE_ARDUINO_OTA
DIAG("ota", ArduinoOTA.handle());
#endif
DIAG("wled", wledHandle());
if (makeFirmwareUpdate) {
makeFirmwareUpdate = false;
doFirmwareUpdate();