271 lines
10 KiB
Arduino
271 lines
10 KiB
Arduino
#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
|