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:
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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -320,6 +320,32 @@
|
||||
<label for="rgbSpeed">Animation Speed</label>
|
||||
<input type="range" min="1" max="255" class="slider" id="rgbSpeed">
|
||||
</div>
|
||||
|
||||
<div class="control-holder wled-holder">
|
||||
<label>SignalRGB (WLED)</label>
|
||||
<input id="wledEnable" type="checkbox" class="switch">
|
||||
<span class="switcher-text left non-selectable" for="wledEnable">off</span>
|
||||
<span class="switcher non-selectable" for="wledEnable"></span>
|
||||
<span class="switcher-text right non-selectable" for="wledEnable">on</span>
|
||||
</div>
|
||||
<div class="control-holder wled-holder">
|
||||
<label>SignalRGB off with tubes</label>
|
||||
<input id="wledDarkWithTubes" type="checkbox" class="switch">
|
||||
<span class="switcher-text left non-selectable" for="wledDarkWithTubes">off</span>
|
||||
<span class="switcher non-selectable" for="wledDarkWithTubes"></span>
|
||||
<span class="switcher-text right non-selectable" for="wledDarkWithTubes">on</span>
|
||||
</div>
|
||||
<div class="control-holder wled-holder">
|
||||
<label>SignalRGB uses RGB Brightness</label>
|
||||
<input id="wledUseBrightness" type="checkbox" class="switch">
|
||||
<span class="switcher-text left non-selectable" for="wledUseBrightness">off</span>
|
||||
<span class="switcher non-selectable" for="wledUseBrightness"></span>
|
||||
<span class="switcher-text right non-selectable" for="wledUseBrightness">on</span>
|
||||
</div>
|
||||
<div class="control-holder wled-holder">
|
||||
<label>SignalRGB active</label>
|
||||
<span id="wledLive" class="info medium"></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- WiFi settings -->
|
||||
<div page=wifi>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 <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
|
||||
@@ -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<PixelCount;i++) {
|
||||
strip.SetPixelColor(i,black); //pixels without a tube stay dark
|
||||
}
|
||||
for (int t=0;t<maxDigits;t++) {
|
||||
const uint8_t* c = rgb + 3*(maxDigits-1-t);
|
||||
setPixels(t, RgbColor(c[0],c[1],c[2]));
|
||||
}
|
||||
neoShow();
|
||||
}
|
||||
|
||||
void doAnimationMakuna() {
|
||||
static unsigned long lastRun = 0;
|
||||
|
||||
if (EEPROMsaving) return;
|
||||
|
||||
|
||||
if (alarmON) {
|
||||
alarmLight();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (wledLive) return; //SignalRGB drives the LEDs (wled.ino)
|
||||
|
||||
if ((prm.rgbEffect <=1) && ((millis()-lastRun)<1000)) return; //fix color
|
||||
if ((millis()-lastRun)<max(FPS_MSEC,258-prm.rgbSpeed)) return;
|
||||
lastRun = millis();
|
||||
@@ -624,13 +651,7 @@ static unsigned long lastRun = 0;
|
||||
}
|
||||
|
||||
colorStep = max(1,prm.rgbSpeed/5);
|
||||
neoBrightness = prm.rgbBrightness;
|
||||
if (autoBrightness) {
|
||||
neoBrightness = (neoBrightness*lx)/long(MAXIMUM_LUX/2);
|
||||
if (neoBrightness< c_MinBrightness)
|
||||
neoBrightness = c_MinBrightness;
|
||||
}
|
||||
neoBrightness = min(neoBrightness,long(RGB_MAX_BRIGHTNESS)); //safety only
|
||||
neoBrightness = neoUserBrightness();
|
||||
strip.SetBrightness(neoBrightness);
|
||||
//DPRINTLN(" NeoBrightness:"); DPRINT(neoBrightness);
|
||||
|
||||
|
||||
Reference in new issue
Block a user