diff --git a/CLAUDE.md b/CLAUDE.md index 4380d4f..ed2e81f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -100,4 +100,11 @@ GPIO0 to GND, tap RST to GND, then release GPIO0. it is now only sent when the pixels change, and only in the dark gap between tubes; this also fixed digits that seemed to hang). Saving the firmware URL no longer starts an update. Optional web password, cathode-protect random start, LED gamma. -6. Open: the user can create `secrets.h` (gitignored, never read it) for OTA and web passwords. +6. Done (2026-10-25): SignalRGB via WLED emulation (`wled.ino`, `USE_WLED_REALTIME`, + `WLED_MAX_FPS` 30: no flicker, no loop stalls). SignalRGB's WLED plugin only sends as many LEDs as the channel's component has (needs 4). LED order checked: + SignalRGB LED 0 = left tube, `tubePixels` {0,1,2,3} is right. Settings `wledEnable` / + `wledDarkWithTubes` / `wledUseBrightness` live in `prmExt`, stored in EEPROM after `prm` with + their own magic value: never add fields to `prm` (a `MAGIC_VALUE` bump factory-resets the + clock, WiFi included). HA switch "SignalRGB". `page.js`/`site.css` were cached by browsers for a + year; now sent with `no-cache`. +7. Open: the user can create `secrets.h` (gitignored, never read it) for OTA and web passwords. diff --git a/README.md b/README.md index 3c0637d..3c0bbae 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,9 @@ WiFi updates and Home Assistant control. - UniClock's web page for all settings - Firmware updates over WiFi (ArduinoOTA) - Home Assistant over MQTT discovery: tubes on/off, day/night brightness, auto dim, backlight - (on/off, brightness, colour), LED effect, cathode protect, light and WiFi-signal sensors + (on/off, brightness, colour), LED effect, cathode protect, SignalRGB on/off, light and + WiFi-signal sensors +- SignalRGB: the backlight can show up in SignalRGB as a WLED device (see below) ## The hardware @@ -70,6 +72,21 @@ and restart the clock; it then shows up under Settings > Devices > MQTT. Optional passwords for the web page and OTA updates go in `firmware/uniclock/ESP32_UniClock2/secrets.h` (see `secrets.h.example`; the file is gitignored). +## SignalRGB + +Turn on **SignalRGB (WLED)** on the RGB page of the web UI (or the SignalRGB switch in Home +Assistant). The clock then answers like a WLED device with one LED per tube, LED 0 = left tube: +mDNS `_wled._tcp`, `/json/info` and `/json/state` without a password, and colour frames over UDP +(WLED realtime on port 21324, DDP on 4048). SignalRGB should find it by itself; if not, add it by +IP address. SignalRGB only drives as many LEDs as the component on the device's channel has, so +give the channel a component with 4 LEDs. While SignalRGB sends colours it controls the LEDs (at most 30 frames per second, +`WLED_MAX_FPS` in `clocks.h`); 2.5 s after it stops, the normal effect comes back. The alarm +flash always wins. Two more switches on the same page: + +- **SignalRGB off with tubes**: the LEDs stay dark at night and while the tubes are off. +- **SignalRGB uses RGB Brightness**: colours are scaled by the RGB brightness and auto dim; + off = colours as SignalRGB sends them. + ## Changes to ESP_UniClock Besides the new driver and configuration: @@ -77,6 +94,8 @@ Besides the new driver and configuration: - NeoPixels on any ESP8266 pin, sent only when they change and between tube slots (the bit-banged update blocks interrupts and made the tubes flicker) - ArduinoOTA; Home Assistant integration rewritten for ArduinoHA 2.1 +- WLED emulation for SignalRGB (`wled.ino`); its settings are stored after the UniClock + settings in EEPROM, so the update keeps all existing settings - EU summer-time rules (upstream ended summer time on the US date); switching DST on no longer shifts the clock by an hour in winter - Saving the firmware URL no longer starts an HTTP update; optional web password diff --git a/firmware/uniclock/ESP32_UniClock2/ESP32_UniClock2.ino b/firmware/uniclock/ESP32_UniClock2/ESP32_UniClock2.ino index 76d4b71..74101d2 100644 --- a/firmware/uniclock/ESP32_UniClock2/ESP32_UniClock2.ino +++ b/firmware/uniclock/ESP32_UniClock2/ESP32_UniClock2.ino @@ -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 @@ -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(); diff --git a/firmware/uniclock/ESP32_UniClock2/clocks.h b/firmware/uniclock/ESP32_UniClock2/clocks.h index 5394144..299a480 100644 --- a/firmware/uniclock/ESP32_UniClock2/clocks.h +++ b/firmware/uniclock/ESP32_UniClock2/clocks.h @@ -147,6 +147,8 @@ byte tubePixels[] = {0, 1, 2, 3}; //4 WS2812, one per tube (chain order not ve //#define LOOP_DIAG //report main-loop stalls in /getCurrentInfos ("stalls"); for debugging only #define USE_ARDUINO_OTA #define OTA_HOSTNAME "nixie-in12" +#define USE_WLED_REALTIME //SignalRGB: shows up as a WLED device (switch on the web page / in HA), wled.ino +#define WLED_MAX_FPS 30 //lower (e.g. 15) if the tubes flicker while SignalRGB is running #if __has_include("secrets.h") #include "secrets.h" //optional, gitignored: DEFAULT_SSID, DEFAULT_PSW, OTA_PASSWORD (see secrets.h.example) #endif diff --git a/firmware/uniclock/ESP32_UniClock2/data/index.html b/firmware/uniclock/ESP32_UniClock2/data/index.html index bccf306..2302434 100644 --- a/firmware/uniclock/ESP32_UniClock2/data/index.html +++ b/firmware/uniclock/ESP32_UniClock2/data/index.html @@ -320,6 +320,32 @@ + +
+ + + off + + on +
+
+ + + off + + on +
+
+ + + off + + on +
+
+ + +
diff --git a/firmware/uniclock/ESP32_UniClock2/data/page.js b/firmware/uniclock/ESP32_UniClock2/data/page.js index 8c47aae..f88f4ed 100644 --- a/firmware/uniclock/ESP32_UniClock2/data/page.js +++ b/firmware/uniclock/ESP32_UniClock2/data/page.js @@ -54,7 +54,11 @@ var controlInfos = { "rgbBrightness": "RGB leds brightness 0..255", "rgbFixColor": "RGB color, if FixColor Effect is used", "rgbSpeed": "RGB animation speed", - + "wledEnable": "Let SignalRGB control the LEDs: the clock shows up there as a WLED device", + "wledDarkWithTubes": "LEDs stay dark while the tubes / backlight are off, also when SignalRGB is running", + "wledUseBrightness": "Scale SignalRGB colours by RGB Brightness (and auto dim). Off: colours as sent", + "wledLive": "SignalRGB is sending colours right now", + //Wifi settings "wifiSsid": "WiFi network SSID to connect", "wifiPsw": "WiFI password", @@ -132,7 +136,11 @@ var configuration = { "rgbBrightness": 100, "rgbFixColor": 150, "rgbSpeed": 50, - + "wledEnable": false, + "wledDarkWithTubes": true, + "wledUseBrightness": true, + "wledLive": false, + //Wifi settings "wifiSsid": "mywifi", "wifiPsw": "mypsw", @@ -232,6 +240,7 @@ function setCurrentInfos(){ $('#currentTime').html(getCurrentDate(configuration["currentDate"]) + " " + configuration["currentTime"]); $('#lux').html(configuration["lux"]); $('#rssi').html(configuration["rssi"]); + $('#wledLive').html(configuration["wledLive"] ? "yes" : "no"); $('#pressure').html(round(configuration["pressure"],1)); $('#humidity').html(round(configuration["humidity"],1)); $('#humidity2').html(round(configuration["humidity2"],1)); @@ -420,7 +429,8 @@ function Init(){ index == 'rgbDir' || index == 'manualOverride' || index == 'enableAutoDim' || index == 'enableRadar' || index == 'enableDoubleBlink' || index == 'enableTimeDisplay' || - index == 'mqttEnable' || index == 'tempCF' || index == "wifiMode" + index == 'mqttEnable' || index == 'tempCF' || index == "wifiMode" || + index == 'wledEnable' || index == 'wledDarkWithTubes' || index == 'wledUseBrightness' ) && !!value ){ $('#'+index).prop('checked',true); @@ -438,6 +448,7 @@ function Init(){ $('.temperature-holder').toggleClass('hidden',configuration['temperature'] == 255); $('.temperature-holder2').toggleClass('hidden',configuration['temperature2'] == 255); $('.rgb-holder').toggleClass('hidden',configuration['rgbEffect'] == 255); + $('.wled-holder').toggleClass('hidden',configuration['wledEnable'] === undefined); //firmware without USE_WLED_REALTIME $('.tube-holder').toggleClass('hidden',configuration['tubeDriver'] == "DUMMY"); $('.wordclock-holder').toggleClass('hidden',configuration['tubeDriver'] == "WORDCLOCK"); $('.sensors-holder').toggleClass('hidden',configuration['temperature'] == 255); diff --git a/firmware/uniclock/ESP32_UniClock2/mqtt.ino b/firmware/uniclock/ESP32_UniClock2/mqtt.ino index 9406d32..47c2644 100644 --- a/firmware/uniclock/ESP32_UniClock2/mqtt.ino +++ b/firmware/uniclock/ESP32_UniClock2/mqtt.ino @@ -11,7 +11,7 @@ // Sensors: temperature/humidity/pressure/lux/radar (whichever exist on this clock), WiFi signal // Controls: Tubes (switch, runtime only), Day/Night brightness (numbers), Auto dim (switch), // Cathode protect (button); with USE_NEOPIXEL also Backlight (light: on/off, brightness, colour) -// and LED effect (select). +// and LED effect (select); with USE_WLED_REALTIME also SignalRGB (switch, = prmExt.wledEnable). // Changes made on the clock's own web page are reported back to HA by mqttSend(). // Settings changed from HA are saved to EEPROM a few seconds after the last change (one flash write per burst). // @@ -85,6 +85,9 @@ HAButton mqttCathodeProtect("cathode_protect"); HASelect mqttLedEffect("led_effect"); byte mqttLastEffect = 2; //effect restored when the backlight is switched on from HA (rainbow flow by default) #endif +#ifdef USE_WLED_REALTIME + HASwitch mqttWled("signalrgb"); +#endif //Bounded copy of an MQTT payload into a zero-terminated buffer. // FIXED: the old onMqttMessage() memcpy'd the payload into a 100-byte stack buffer without a length check or a @@ -263,6 +266,14 @@ void onLedEffectCommand(int8_t index, HASelect* sender) { } #endif +#ifdef USE_WLED_REALTIME +void onWledCommand(bool state, HASwitch* sender) { + prmExt.wledEnable = state; //wledHandle() starts/stops the listeners + sender->setState(state); + mqttRequestSave(); +} +#endif + void setupMqtt() { char mactmp[20]; @@ -364,6 +375,12 @@ void setupMqtt() { mqttLedEffect.onCommand(onLedEffectCommand); #endif + #ifdef USE_WLED_REALTIME + mqttWled.setName("SignalRGB"); + mqttWled.setIcon("mdi:monitor-shimmer"); + mqttWled.onCommand(onWledCommand); + #endif + mqtt.onMessage(onMqttMessage); mqtt.onConnected(onMqttConnected); @@ -405,6 +422,9 @@ void mqttReportState() { mqttBacklight.setRGBColor(wheelToRgb(prm.rgbFixColor)); mqttLedEffect.setState(prm.rgbEffect); #endif + #ifdef USE_WLED_REALTIME + mqttWled.setState(prmExt.wledEnable); + #endif } void mqttSend() { diff --git a/firmware/uniclock/ESP32_UniClock2/wled.ino b/firmware/uniclock/ESP32_UniClock2/wled.ino new file mode 100644 index 0000000..e700536 --- /dev/null +++ b/firmware/uniclock/ESP32_UniClock2/wled.ino @@ -0,0 +1,271 @@ +#ifdef USE_WLED_REALTIME +//========================================================================================================================= +// wled.ino - lets SignalRGB (or any other WLED client) drive the tube backlight, by answering like a WLED device +// (compiled only if USE_WLED_REALTIME is defined for the active CLOCK_NN in clocks.h; ESP8266 + USE_NEOPIXEL only) +// +// SignalRGB's WLED support needs three things, all provided here while prmExt.wledEnable is set: +// - mDNS service "_wled._tcp" on port 80 with a "mac" TXT record (added to ArduinoOTA's mDNS responder, so it needs +// USE_ARDUINO_OTA; without it the clock can still be added in SignalRGB by IP address) +// - the read-only part of WLED's JSON API: GET /json/info (LED count = number of tubes, name, MAC), /json/state +// and /json. POSTs are acknowledged and ignored. No password, as SignalRGB can't send one. +// - realtime frames over UDP: WLED's WARLS/DRGB/DNRGB on port 21324 (byte 1 = timeout in seconds) and DDP on 4048. +// LED 0 is the leftmost tube. While frames arrive (wledLive), doAnimationMakuna() leaves the LEDs alone; the alarm +// flash still wins. After the timeout the normal effect comes back by itself. +// Frames are shown at most WLED_MAX_FPS times a second: every WS2812 update blocks interrupts and waits for a dark +// gap between tubes (neoShow()), so fewer updates mean less risk of tube flicker. +// Settings (web page and the Home Assistant "SignalRGB" switch): prmExt.wledEnable, prmExt.wledDarkWithTubes (dark +// while the backlight would be off: night, radar, HA Tubes switch), prmExt.wledUseBrightness (scale by the RGB +// brightness and auto dim instead of only the RGB_MAX_BRIGHTNESS cap). +//_______________________________________________________________________________________________________________________ + +#if !defined(ESP8266) || !defined(USE_NEOPIXEL) + #error "USE_WLED_REALTIME needs an ESP8266 and USE_NEOPIXEL" +#endif + +#include + +#ifndef WLED_MAX_FPS + #define WLED_MAX_FPS 30 +#endif +#define WLED_UDP_PORT 21324 //WLED realtime (WARLS/DRGB/DRGBW/DNRGB) +#define WLED_DDP_PORT 4048 //DDP +#define WLED_DEFAULT_TIMEOUT 2500 //ms without frames before the normal effect returns (WLED's default) +#define WLED_MAX_TIMEOUT 10000 //cap for the timeout a packet asks for (255 = "forever" in WLED) +#define WLED_MAX_PACKETS 8 //packets read per wledHandle() call, so a burst can't stall the loop + +WiFiUDP wledUdp; +WiFiUDP wledDdp; +MDNSResponder::hMDNSService wledMdnsService = nullptr; +boolean wledReady = false; //set by wledSetup(): WiFi and mDNS are up +boolean wledRunning = false; //sockets open +uint8_t wledFrame[BUFSIZE * 3]; //newest frame, R,G,B per tube, leftmost tube first +uint8_t wledPacket[128]; //plenty for a few LEDs; longer packets are cut off, which only drops LEDs we don't have +unsigned long wledLastPacket = 0; +unsigned long wledTimeout = WLED_DEFAULT_TIMEOUT; +char wledMac[13]; //MAC as 12 lowercase hex digits, as WLED reports it + +//Copies pixel data that starts at byte offset `offset` of the strip into wledFrame. +static void wledWrite(size_t offset, const uint8_t* data, size_t len) { + size_t size = (size_t)maxDigits * 3; + if (offset >= size) return; + memcpy(wledFrame + offset, data, min(len, size - offset)); +} + +//Returns true if the packet held pixel data. +static boolean wledParseRealtime(const uint8_t* p, int len) { + if (len < 2) return false; + byte t = p[1]; + wledTimeout = (t == 0) ? WLED_DEFAULT_TIMEOUT : min((unsigned long)t * 1000ul, (unsigned long)WLED_MAX_TIMEOUT); + switch (p[0]) { + case 1: //WARLS: index, R, G, B + for (int i = 2; i + 4 <= len; i += 4) wledWrite((size_t)p[i] * 3, p + i + 1, 3); + return true; + case 2: //DRGB: R, G, B from LED 0 + wledWrite(0, p + 2, len - 2); + return true; + case 3: //DRGBW: R, G, B, W from LED 0 (W dropped) + for (int i = 2, led = 0; i + 4 <= len; i += 4, led++) wledWrite((size_t)led * 3, p + i, 3); + return true; + case 4: //DNRGB: start index (16 bit), then R, G, B + if (len < 4) return false; + wledWrite((size_t)((p[2] << 8) | p[3]) * 3, p + 4, len - 4); + return true; + } + return false; +} + +static boolean wledParseDdp(const uint8_t* p, int len) { + if ((len < 10) || ((p[0] & 0xC0) != 0x40)) return false; //DDP version 1 + int header = (p[0] & 0x10) ? 14 : 10; //+4 bytes with a timecode + if (len < header) return false; + uint32_t offset = ((uint32_t)p[4] << 24) | ((uint32_t)p[5] << 16) | ((uint32_t)p[6] << 8) | p[7]; + int dataLen = min((p[8] << 8) | p[9], len - header); + wledTimeout = WLED_DEFAULT_TIMEOUT; + wledWrite(offset, p + header, dataLen); + return true; +} + +static void wledReceive(WiFiUDP& udp, boolean ddp) { + for (int n = 0; n < WLED_MAX_PACKETS; n++) { + int size = udp.parsePacket(); + if (size <= 0) return; + int len = udp.read(wledPacket, sizeof(wledPacket)); + if (len <= 0) continue; + if (ddp ? wledParseDdp(wledPacket, len) : wledParseRealtime(wledPacket, len)) { + if (!wledLive) DPRINTLN("WLED: realtime frames started"); + wledLive = true; + wledLastPacket = millis(); + } + } +} + +static void wledBegin() { + wledUdp.begin(WLED_UDP_PORT); + wledDdp.begin(WLED_DDP_PORT); + if (MDNS.isRunning() && !wledMdnsService) { + wledMdnsService = MDNS.addService(nullptr, "wled", "tcp", 80); + if (wledMdnsService) MDNS.addServiceTxt(wledMdnsService, "mac", wledMac); + } + memset(wledFrame, 0, sizeof(wledFrame)); + wledRunning = true; + DPRINTLN("WLED: listening for SignalRGB"); +} + +static void wledEnd() { + wledUdp.stop(); + wledDdp.stop(); + if (wledMdnsService) { + MDNS.removeService(wledMdnsService); + wledMdnsService = nullptr; + } + wledRunning = false; + wledLive = false; + DPRINTLN("WLED: stopped"); +} + +void wledSetup() { + uint8_t m[6]; + WiFi.macAddress(m); + sprintf(wledMac, "%02x%02x%02x%02x%02x%02x", m[0], m[1], m[2], m[3], m[4], m[5]); + wledReady = true; +} + +//Called from loop() and Fdelay(). Follows prmExt.wledEnable, reads frames and shows them. +void wledHandle() { + static unsigned long lastShow = 0; + + if (!wledReady) return; + if (prmExt.wledEnable != wledRunning) { + if (prmExt.wledEnable) wledBegin(); + else wledEnd(); + } + if (!wledRunning) return; + + wledReceive(wledUdp, false); + wledReceive(wledDdp, true); + if (!wledLive) return; + + if ((millis() - wledLastPacket) > wledTimeout) { + wledLive = false; //doAnimationMakuna() takes over again + DPRINTLN("WLED: realtime frames stopped"); + return; + } + if (alarmON || EEPROMsaving) return; //the alarm flash wins + if ((millis() - lastShow) < (1000ul / WLED_MAX_FPS)) return; + lastShow = millis(); + + long bri = prmExt.wledUseBrightness ? neoUserBrightness() : long(RGB_MAX_BRIGHTNESS); + if (prmExt.wledDarkWithTubes && (!displayON || !radarON || !tubesOn)) bri = 0; + neoShowFrame(wledFrame, bri); //unchanged frames are not sent again (neoShow()) +} + +//--- WLED JSON API ------------------------------------------------------------------------------------------------------ + +static void wledInfo(JsonObject o) { + o["ver"] = "0.14.4"; + o["vid"] = 2405180; + JsonObject leds = o.createNestedObject("leds"); + leds["count"] = maxDigits; + leds["pwr"] = 0; + leds["fps"] = WLED_MAX_FPS; + leds["maxpwr"] = 0; + leds["maxseg"] = 1; + leds.createNestedArray("seglc").add(1); + leds["lc"] = 1; + leds["rgbw"] = false; + leds["wv"] = 0; + leds["cct"] = 0; + o["str"] = false; + o["name"] = webName; + o["udpport"] = WLED_UDP_PORT; + o["live"] = wledLive; + o["liveseg"] = -1; + o["lm"] = ""; + o["lip"] = ""; + o["ws"] = 0; + o["fxcount"] = 1; + o["palcount"] = 1; + JsonObject wifi = o.createNestedObject("wifi"); + wifi["bssid"] = WiFi.BSSIDstr(); + wifi["rssi"] = WiFi.RSSI(); + wifi["signal"] = constrain(2 * (WiFi.RSSI() + 100), 0, 100); + wifi["channel"] = WiFi.channel(); + o["arch"] = "esp8266"; + o["core"] = ESP.getCoreVersion(); + o["freeheap"] = ESP.getFreeHeap(); + o["uptime"] = millis() / 1000; + o["opt"] = 0; + o["brand"] = "WLED"; + o["product"] = "FOSS"; + o["mac"] = wledMac; + o["ip"] = WiFi.localIP().toString(); +} + +static void wledState(JsonObject o) { + o["on"] = true; + o["bri"] = 255; + o["transition"] = 0; + o["ps"] = -1; + o["pl"] = -1; + o["live"] = wledLive; + o["lor"] = 0; + JsonObject seg = o.createNestedArray("seg").createNestedObject(); + seg["id"] = 0; + seg["start"] = 0; + seg["stop"] = maxDigits; + seg["len"] = maxDigits; + seg["grp"] = 1; + seg["spc"] = 0; + seg["on"] = true; + seg["bri"] = 255; + JsonArray col = seg.createNestedArray("col"); + for (int i = 0; i < 3; i++) { + JsonArray c = col.createNestedArray(); + byte v = (i == 0) ? 255 : 0; + c.add(v); c.add(v); c.add(v); + } + seg["fx"] = 0; + seg["sx"] = 128; + seg["ix"] = 128; + seg["pal"] = 0; + seg["sel"] = true; + seg["rev"] = false; + seg["mi"] = false; +} + +//what: 'i' = /json/info, 's' = /json/state, 'a' = /json (everything) +static void wledSendJson(AsyncWebServerRequest *request, char what) { + if (!prmExt.wledEnable) { + request->send(404, "text/plain", "WLED emulation is off"); + return; + } + DynamicJsonDocument doc(1536); + if (what == 'i') wledInfo(doc.to()); + else if (what == 's') wledState(doc.to()); + else { + wledState(doc.createNestedObject("state")); + wledInfo(doc.createNestedObject("info")); + doc.createNestedArray("effects").add("Solid"); + doc.createNestedArray("palettes").add("Default"); + } + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +} + +//Registered in startServer(), before onNotFound. "/json" also matches "/json/...", so it comes last. +void wledRoutes() { + server.on("/json/info", HTTP_GET, [](AsyncWebServerRequest *request) { wledSendJson(request, 'i'); }); + server.on("/json/state", HTTP_GET | HTTP_POST, [](AsyncWebServerRequest *request) { wledSendJson(request, 's'); }); + server.on("/json", HTTP_GET, [](AsyncWebServerRequest *request) { wledSendJson(request, 'a'); }); + server.on("/json", HTTP_POST, [](AsyncWebServerRequest *request) { + if (prmExt.wledEnable) request->send(200, "application/json", "{\"success\":true}"); + else request->send(404, "text/plain", "WLED emulation is off"); + }); +} + +#else + void wledSetup() {} + void wledHandle() {} + void wledRoutes() {} +#endif diff --git a/firmware/uniclock/ESP32_UniClock2/z_neopixel.ino b/firmware/uniclock/ESP32_UniClock2/z_neopixel.ino index 774fa56..e8eeb8e 100644 --- a/firmware/uniclock/ESP32_UniClock2/z_neopixel.ino +++ b/firmware/uniclock/ESP32_UniClock2/z_neopixel.ino @@ -603,16 +603,43 @@ void kitt() { } +//prm.rgbBrightness, scaled by the light sensor when auto dim is on. Also used by wled.ino. +long neoUserBrightness() { + long b = prm.rgbBrightness; + if (autoBrightness) { + b = (b*lx)/long(MAXIMUM_LUX/2); + if (b < c_MinBrightness) + b = c_MinBrightness; + } + return min(b,long(RGB_MAX_BRIGHTNESS)); //safety only +} + +//Shows a frame from wled.ino: rgb[] holds one R,G,B triple per tube, leftmost tube first +//(digit position 0 is the rightmost tube, hence the reversed index). +void neoShowFrame(const uint8_t* rgb, long brightness) { + strip.SetBrightness(brightness); //before SetPixelColor(), which scales by it + for (int i=0;i