Files

271 lines
10 KiB
Arduino
Raw Permalink Normal View History

2026-10-25 23:32:25 +01:00
#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 <ESP8266mDNS.h>
#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<JsonObject>());
else if (what == 's') wledState(doc.to<JsonObject>());
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