#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