From 088372115eaee70e5d06404e8e46a2c92c703ba8 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Thu, 6 Aug 2026 23:42:17 +0100 Subject: [PATCH 1/2] Split udp.cpp into per-protocol files, encapsulate two low-fanout globals udp.cpp mixed at least 6 unrelated wire protocols (WLED's own Notifier sync, node discovery, Hyperion, TPM2.NET, legacy UDP realtime, Art-Net/DDP/E1.31 sender, ESP-NOW transport) in one 987-line file, mostly interleaved inside a single handleNotifications() dispatcher with nested early-returns. Split each protocol into its own file with handleNotifications() left as a thin dispatcher: - sync_notifier.cpp WLED's own state-sync protocol - sync_nodes.cpp node discovery - realtime.cpp shared realtime lock/unlock state machine - realtime_udp.cpp Hyperion / TPM2.NET / legacy UDP realtime - realtime_broadcast.cpp outbound Art-Net/DDP/E1.31 sender - espnow_sync.cpp ESP-NOW transport for the notifier protocol - sync.h constants shared by the above - udp.cpp handleNotifications() dispatcher only Every extraction preserves the original control flow exactly; bare `return;` inside the old inline blocks map 1:1 to `return true/false;` in the new boolean-returning helpers. Also encapsulated two clusters of WLED_GLOBAL state that turned out to be used only within this subsystem once split out: - tpmPacketCount / tpmPayloadFrameSize were referenced only in realtime_udp.cpp; now file-local statics instead of globals. - notificationCount / notificationSentCallMode / notificationSentTime were referenced only in udp.cpp + sync_notifier.cpp; now a NotifierSendState struct private to sync_notifier.cpp, exposed via a single notifyRetryIfNeeded() call instead of raw field access. Verified: nodemcuv2 (ESP8266) builds and links cleanly, including the wled_espnow usermod linking against the relocated ESP-NOW callbacks. esp32dev could not be verified in this environment due to a sandbox network issue unrelated to this change (opened as draft pending that). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01UtCyBD91vAYWvBzaMyQSHd --- wled00/espnow_sync.cpp | 109 ++++ wled00/fcn_declare.h | 25 +- wled00/realtime.cpp | 61 +++ wled00/realtime_broadcast.cpp | 153 ++++++ wled00/realtime_udp.cpp | 134 +++++ wled00/sync.h | 21 + wled00/sync_nodes.cpp | 106 ++++ wled00/sync_notifier.cpp | 414 +++++++++++++++ wled00/udp.cpp | 926 +--------------------------------- wled00/wled.h | 7 +- 10 files changed, 1036 insertions(+), 920 deletions(-) create mode 100644 wled00/espnow_sync.cpp create mode 100644 wled00/realtime.cpp create mode 100644 wled00/realtime_broadcast.cpp create mode 100644 wled00/realtime_udp.cpp create mode 100644 wled00/sync.h create mode 100644 wled00/sync_nodes.cpp create mode 100644 wled00/sync_notifier.cpp diff --git a/wled00/espnow_sync.cpp b/wled00/espnow_sync.cpp new file mode 100644 index 0000000000..8e2a73bea2 --- /dev/null +++ b/wled00/espnow_sync.cpp @@ -0,0 +1,109 @@ +#include "wled.h" +#include "sync.h" + +/* + * ESP-NOW transport for the WLED Notifier sync protocol. Not a protocol of its own - + * reassembles fragmented packets and hands the result to parseNotifyPacket() + * (sync_notifier.cpp), the same decoder used by the UDP transport. + */ + +#ifndef WLED_DISABLE_ESPNOW +// ESP-NOW message sent callback function +void espNowSentCB(uint8_t* address, uint8_t status) { + DEBUG_PRINTF_P(PSTR("Message sent to " MACSTR ", status: %d\n"), MAC2STR(address), status); +} + +// ESP-NOW message receive callback function +void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rssi, bool broadcast) { + sprintf_P(last_signal_src, PSTR("%02x%02x%02x%02x%02x%02x"), address[0], address[1], address[2], address[3], address[4], address[5]); + + #ifdef WLED_DEBUG + DEBUG_PRINT(F("ESP-NOW: ")); DEBUG_PRINT(last_signal_src); DEBUG_PRINT(F(" -> ")); DEBUG_PRINTLN(len); + for (int i=0; i(data); + if (len < 3 || !broadcast || buffer->magic != 'W' || !useESPNowSync || WLED_CONNECTED) { + DEBUG_PRINTLN(F("ESP-NOW unexpected packet, not syncing or connected to WiFi.")); + return; + } + + static uint8_t *udpIn = nullptr; + static uint8_t packetsReceived = 0; + static uint8_t segsReceived = 0; + static unsigned long lastProcessed = 0; + + if (buffer->packet == 0) { + packetsReceived = 0; // it will increment later (this is to make sure we start counting packets correctly) + if (udpIn == nullptr) { + udpIn = (uint8_t *)malloc(WLEDPACKETSIZE); // we cannot use stack as we are in callback + if (!udpIn) return; // memory alocation failed + DEBUG_PRINTLN(F("ESP-NOW inited UDP buffer.")); + } + memcpy(udpIn, buffer->data, len-3); // global data (41 bytes + up to 5 segments) + segsReceived = (len - 3 - 41) / UDP_SEG_SIZE; + } else if (buffer->packet == packetsReceived && udpIn && ((len - 3) / UDP_SEG_SIZE) * UDP_SEG_SIZE == (len-3)) { + // we received a packet full of segments + if (segsReceived >= MAX_NUM_SEGMENTS) { + // we are already past max segments, just ignore + DEBUG_PRINTLN(F("ESP-NOW received segments past maximum.")); + len = 3; + } else if ((segsReceived + ((len - 3) / UDP_SEG_SIZE)) >= MAX_NUM_SEGMENTS) { + len = ((MAX_NUM_SEGMENTS - segsReceived) * UDP_SEG_SIZE) + 3; // we have reached max number of segments + } + if (len > 3) { + memcpy(udpIn + 41 + (segsReceived * UDP_SEG_SIZE), buffer->data, len-3); + segsReceived += (len - 3) / UDP_SEG_SIZE; + } + } else { + // any out of order packet or incorrectly sized packet or if we have no UDP buffer will abort + DEBUG_PRINTF_P(PSTR("ESP-NOW incorrect packet: %d (%d) [%d]\n"), (int)buffer->packet, (int)len-3, (int)UDP_SEG_SIZE); + if (udpIn) free(udpIn); + udpIn = nullptr; + packetsReceived = 0; + segsReceived = 0; + return; + } + if (!udpIn) return; + + packetsReceived++; + DEBUG_PRINTF_P(PSTR("ESP-NOW packet received: %d (%d/%d) s:[%d/%d]\n"), (int)buffer->packet, (int)packetsReceived, (int)buffer->noOfPackets, (int)segsReceived, MAX_NUM_SEGMENTS); + if (packetsReceived >= buffer->noOfPackets) { + // last packet received + if (millis() - lastProcessed > 250) { + DEBUG_PRINTLN(F("ESP-NOW processing complete message.")); + parseNotifyPacket(udpIn); + lastProcessed = millis(); + } else { + DEBUG_PRINTLN(F("ESP-NOW ignoring complete message.")); + } + free(udpIn); + udpIn = nullptr; + packetsReceived = 0; + segsReceived = 0; + } +} +#endif diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 18327eb877..9fdd7d72c6 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -295,14 +295,31 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage); bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply=true); //udp.cpp +void handleNotifications(); + +//sync_notifier.cpp void notify(byte callMode, bool followUp=false); -uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t* buffer, uint8_t bri=255, bool isRGBW=false); +void notifyRetryIfNeeded(); +void parseNotifyPacket(const uint8_t *udpIn); + +//sync_nodes.cpp +bool parseNodeInfoPacket(const uint8_t *udpIn, unsigned len, bool isSupp, const IPAddress &localIP); +void refreshNodeList(); +void sendSysInfoUDP(); + +//realtime.cpp void realtimeLock(uint32_t timeoutMs, byte md = REALTIME_MODE_GENERIC); void exitRealtime(); -void handleNotifications(); void setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w); -void refreshNodeList(); -void sendSysInfoUDP(); + +//realtime_udp.cpp +bool handleHyperionPacket(); +bool handleDirectRealtimePacket(uint8_t *udpIn, size_t packetSize, bool isSupp); + +//realtime_broadcast.cpp +uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t* buffer, uint8_t bri=255, bool isRGBW=false); + +//espnow_sync.cpp #ifndef WLED_DISABLE_ESPNOW void espNowSentCB(uint8_t* address, uint8_t status); void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rssi, bool broadcast); diff --git a/wled00/realtime.cpp b/wled00/realtime.cpp new file mode 100644 index 0000000000..d9cd90d954 --- /dev/null +++ b/wled00/realtime.cpp @@ -0,0 +1,61 @@ +#include "wled.h" + +/* + * Realtime state machine shared by every "external source pushes raw pixel data at us" + * protocol (Hyperion, TPM2.NET, legacy UDP realtime - see realtime_udp.cpp). This is + * infrastructure, not a protocol of its own. + */ + +// realtimeLock() is called from UDP notifications, JSON API or serial Ada +void realtimeLock(uint32_t timeoutMs, byte md) +{ + if (!realtimeMode && !realtimeOverride) { + if (useMainSegmentOnly) { + Segment& mainseg = strip.getMainSegment(); + mainseg.clear(); // clear entire segment (in case sender transmits less pixels) + mainseg.freeze = true; + // if WLED was off and using main segment only, freeze non-main segments so they stay off + if (bri == 0) { + for (size_t s = 0; s < strip.getSegmentsNum(); s++) strip.getSegment(s).freeze = true; + } + } else { + // clear entire strip + strip.fill(BLACK); + } + // if strip is off (bri==0) and not already in RTM + if (briT == 0) { + strip.setBrightness(briLast, true); + } + } + + if (realtimeTimeout != UINT32_MAX) { + realtimeTimeout = (timeoutMs == 255001 || timeoutMs == 65000) ? UINT32_MAX : millis() + timeoutMs; + } + realtimeMode = md; + + if (realtimeOverride) return; + if (arlsForceMaxBri) strip.setBrightness(255, true); + if (briT > 0 && md == REALTIME_MODE_GENERIC) strip.show(); +} + +void exitRealtime() { + if (!realtimeMode) return; + if (realtimeOverride == REALTIME_OVERRIDE_ONCE) realtimeOverride = REALTIME_OVERRIDE_NONE; + strip.setBrightness(bri, true); + realtimeTimeout = 0; // cancel realtime mode immediately + realtimeMode = REALTIME_MODE_INACTIVE; // inform UI immediately + realtimeIP[0] = 0; + if (useMainSegmentOnly) { // unfreeze live segment again + strip.getMainSegment().freeze = false; + strip.trigger(); + } else { + strip.show(); // possible fix for #3589 + } + updateInterfaces(CALL_MODE_WS_SEND); +} + +void setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w) +{ + unsigned pix = i + arlsOffset; + strip.setRealtimePixelColor(pix, RGBW32(r,g,b,w)); +} diff --git a/wled00/realtime_broadcast.cpp b/wled00/realtime_broadcast.cpp new file mode 100644 index 0000000000..47ed9eab4d --- /dev/null +++ b/wled00/realtime_broadcast.cpp @@ -0,0 +1,153 @@ +#include "wled.h" + +/*********************************************************************************************\ + * Art-Net, DDP, E131 output - work in progress + * (this is the outbound/sender side; see e131.cpp for the inbound receiver side of these + * same wire protocols) +\*********************************************************************************************/ + +// +// Send real time UDP updates to the specified client +// +// type - protocol type (0=DDP, 1=E1.31, 2=ArtNet) +// client - the IP address to send to +// length - the number of pixels +// buffer - a buffer of at least length*4 bytes long +// isRGBW - true if the buffer contains 4 components per pixel + +static size_t sequenceNumber = 0; // this needs to be shared across all outputs +static const size_t ART_NET_HEADER_SIZE = 12; +static const byte ART_NET_HEADER[] PROGMEM = {0x41,0x72,0x74,0x2d,0x4e,0x65,0x74,0x00,0x00,0x50,0x00,0x0e}; + +uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t *buffer, uint8_t bri, bool isRGBW) { + if (!(apActive || interfacesInited) || !client[0] || !length) return 1; // network not initialised or dummy/unset IP address 031522 ajn added check for ap + + WiFiUDP ddpUdp; + + switch (type) { + case 0: // DDP + { + // calculate the number of UDP packets we need to send + size_t channelCount = length * (isRGBW? 4:3); // 1 channel for every R,G,B value + size_t packetCount = ((channelCount-1) / DDP_CHANNELS_PER_PACKET) +1; + + // there are 3 channels per RGB pixel + uint32_t channel = 0; // TODO: allow specifying the start channel + // the current position in the buffer + size_t bufferOffset = 0; + + for (size_t currentPacket = 0; currentPacket < packetCount; currentPacket++) { + if (sequenceNumber > 15) sequenceNumber = 0; + + if (!ddpUdp.beginPacket(client, DDP_DEFAULT_PORT)) { // port defined in ESPAsyncE131.h + //DEBUG_PRINTLN(F("WiFiUDP.beginPacket returned an error")); + return 1; // problem + } + + // the amount of data is AFTER the header in the current packet + size_t packetSize = DDP_CHANNELS_PER_PACKET; + + uint8_t flags = DDP_FLAGS_VER1; + if (currentPacket == (packetCount - 1U)) { + // last packet, set the push flag + // TODO: determine if we want to send an empty push packet to each destination after sending the pixel data + flags = DDP_FLAGS_VER1 | DDP_FLAGS_PUSH; + if (channelCount % DDP_CHANNELS_PER_PACKET) { + packetSize = channelCount % DDP_CHANNELS_PER_PACKET; + } + } + + // write the header + /*0*/ddpUdp.write(flags); + // TODO: sequence number should be 1-15 as 0 means "unused", it has no bad consequences other than out of sequence packet may be accepted + /*1*/ddpUdp.write(sequenceNumber++ & 0x0F); // sequence may be unnecessary unless we are sending twice (as requested in Sync settings) + /*2*/ddpUdp.write(isRGBW ? DDP_TYPE_RGBW32 : DDP_TYPE_RGB24); + /*3*/ddpUdp.write(DDP_ID_DISPLAY); + // data offset in bytes, 32-bit number, MSB first + /*4*/ddpUdp.write(0xFF & (channel >> 24)); + /*5*/ddpUdp.write(0xFF & (channel >> 16)); + /*6*/ddpUdp.write(0xFF & (channel >> 8)); + /*7*/ddpUdp.write(0xFF & (channel )); + // data length in bytes, 16-bit number, MSB first + /*8*/ddpUdp.write(0xFF & (packetSize >> 8)); + /*9*/ddpUdp.write(0xFF & (packetSize )); + + // write the colors, the write write(const uint8_t *buffer, size_t size) + // function is just a loop internally too + for (size_t i = 0; i < packetSize; i += (isRGBW?4:3)) { + ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // R + ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // G + ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // B + if (isRGBW) ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // W + } + + if (!ddpUdp.endPacket()) { + //DEBUG_PRINTLN(F("WiFiUDP.endPacket returned an error")); + return 1; // problem + } + + channel += packetSize; + } + } break; + + case 1: //E1.31 + { + } break; + + case 2: //ArtNet + { + // calculate the number of UDP packets we need to send + const size_t channelCount = length * (isRGBW?4:3); // 1 channel for every R,G,B,(W?) value + const size_t ARTNET_CHANNELS_PER_PACKET = isRGBW?512:510; // 512/4=128 RGBW LEDs, 510/3=170 RGB LEDs + const size_t packetCount = ((channelCount-1)/ARTNET_CHANNELS_PER_PACKET)+1; + + uint32_t channel = 0; + size_t bufferOffset = 0; + + sequenceNumber++; + + for (size_t currentPacket = 0; currentPacket < packetCount; currentPacket++) { + + if (sequenceNumber > 255) sequenceNumber = 0; + + if (!ddpUdp.beginPacket(client, ARTNET_DEFAULT_PORT)) { + DEBUG_PRINTLN(F("Art-Net WiFiUDP.beginPacket returned an error")); + return 1; // borked + } + + size_t packetSize = ARTNET_CHANNELS_PER_PACKET; + + if (currentPacket == (packetCount - 1U)) { + // last packet + if (channelCount % ARTNET_CHANNELS_PER_PACKET) { + packetSize = channelCount % ARTNET_CHANNELS_PER_PACKET; + } + } + + byte header_buffer[ART_NET_HEADER_SIZE]; + memcpy_P(header_buffer, ART_NET_HEADER, ART_NET_HEADER_SIZE); + ddpUdp.write(header_buffer, ART_NET_HEADER_SIZE); // This doesn't change. Hard coded ID, OpCode, and protocol version. + ddpUdp.write(sequenceNumber & 0xFF); // sequence number. 1..255 + ddpUdp.write(0x00); // physical - more an FYI, not really used for anything. 0..3 + ddpUdp.write((currentPacket) & 0xFF); // Universe LSB. 1 full packet == 1 full universe, so just use current packet number. + ddpUdp.write(0x00); // Universe MSB, unused. + ddpUdp.write(0xFF & (packetSize >> 8)); // 16-bit length of channel data, MSB + ddpUdp.write(0xFF & (packetSize )); // 16-bit length of channel data, LSB + + for (size_t i = 0; i < packetSize; i += (isRGBW?4:3)) { + ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // R + ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // G + ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // B + if (isRGBW) ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // W + } + + if (!ddpUdp.endPacket()) { + DEBUG_PRINTLN(F("Art-Net WiFiUDP.endPacket returned an error")); + return 1; // borked + } + channel += packetSize; + } + } break; + } + return 0; +} diff --git a/wled00/realtime_udp.cpp b/wled00/realtime_udp.cpp new file mode 100644 index 0000000000..69721cb9db --- /dev/null +++ b/wled00/realtime_udp.cpp @@ -0,0 +1,134 @@ +#include "wled.h" +#include "sync.h" + +/* + * Direct/legacy realtime pixel-push protocols: Hyperion (raw RGB), TPM2.NET, and WLED's + * own legacy UDP realtime formats (WARLS/DRGB/DRGBW/DNRGB/DNRGBW). Three unrelated wire + * formats that share the realtime state machine in realtime.cpp. + */ + +#define TMP2NET_OUT_PORT 65442 + +// TPM2.NET frame-reassembly state - private to this file, no other translation unit +// needs it (previously these were WLED_GLOBAL, a leftover from when this logic lived +// inline inside handleNotifications()). +static uint8_t tpmPacketCount = 0; +static uint16_t tpmPayloadFrameSize = 0; + +static void sendTPM2Ack() { + notifierUdp.beginPacket(notifierUdp.remoteIP(), TMP2NET_OUT_PORT); + uint8_t response_ack = 0xac; + notifierUdp.write(&response_ack, 1); + notifierUdp.endPacket(); +} + +// hyperion / raw RGB. Reads its own socket independently of the notifier sockets. +// Returns true if a packet was read (handled or discarded), false if nothing was pending. +bool handleHyperionPacket() +{ + size_t packetSize = rgbUdp.parsePacket(); + if (!packetSize) return false; + + if (!receiveDirect) return true; + if (packetSize > UDP_IN_MAXSIZE || packetSize < 3) return true; // packetSize must not exceed buffersize (UDP_IN_MAXSIZE) + realtimeIP = rgbUdp.remoteIP(); + DEBUG_PRINTLN(rgbUdp.remoteIP()); + uint8_t lbuf[packetSize]; + rgbUdp.read(lbuf, packetSize); + realtimeLock(realtimeTimeoutMs, REALTIME_MODE_HYPERION); + if (realtimeOverride) return true; + unsigned totalLen = strip.getLengthTotal(); + for (size_t i = 0, id = 0; i < packetSize -2 && id < totalLen; i += 3, id++) { + setRealtimePixel(id, lbuf[i], lbuf[i+1], lbuf[i+2], 0); + } + if (useMainSegmentOnly) strip.trigger(); + else strip.show(); + return true; +} + +// TPM2.NET and legacy WLED UDP realtime (1 warls 2 drgb 3 drgbw 4 dnrgb 5 dnrgbw). +// Returns true if the packet matched one of these formats (and was consumed), false if +// the caller should keep trying other packet interpretations (e.g. the WLED notifier +// protocol or the JSON/HTTP-over-UDP API). +bool handleDirectRealtimePacket(uint8_t *udpIn, size_t packetSize, bool isSupp) +{ + //TPM2.NET + if (udpIn[0] == 0x9c) { + //WARNING: this code assumes that the final TMP2.NET payload is evenly distributed if using multiple packets (ie. frame size is constant) + //if the number of LEDs in your installation doesn't allow that, please include padding bytes at the end of the last packet + byte tpmType = udpIn[1]; + if (tpmType == 0xaa) { //TPM2.NET polling, expect answer + sendTPM2Ack(); return true; + } + if (tpmType != 0xda) return true; //ignore, not TPM2.NET data + + realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP(); + realtimeLock(realtimeTimeoutMs, REALTIME_MODE_TPM2NET); + if (realtimeOverride) return true; + + tpmPacketCount++; //increment the packet count + if (tpmPacketCount == 1) tpmPayloadFrameSize = (udpIn[2] << 8) + udpIn[3]; //save frame size for the whole payload if this is the first packet + byte packetNum = udpIn[4]; //starts with 1! + byte numPackets = udpIn[5]; + + unsigned id = (tpmPayloadFrameSize/3)*(packetNum-1); //start LED + unsigned totalLen = strip.getLengthTotal(); + // Clamp to prevent buffer overread: loop accesses up to udpIn[tpmPayloadFrameSize + 5] + size_t currentPayloadFrameSize = (packetSize >= 5) ? min(tpmPayloadFrameSize, uint16_t(packetSize - 5)) : 0; + for (size_t i = 6; i < currentPayloadFrameSize + 4U && id < totalLen; i += 3, id++) { + setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); + } + if (tpmPacketCount == numPackets) { //reset packet count and show if all packets were received + tpmPacketCount = 0; + if (useMainSegmentOnly) strip.trigger(); + else strip.show(); + } + return true; + } + + //UDP realtime: 1 warls 2 drgb 3 drgbw 4 dnrgb 5 dnrgbw + if (udpIn[0] > 0 && udpIn[0] < 6) { + realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP(); + DEBUG_PRINTLN(realtimeIP); + if (packetSize < 2) return true; + + if (udpIn[1] == 0) { + realtimeTimeout = 0; // cancel realtime mode immediately + return true; + } else { + realtimeLock(udpIn[1]*1000 +1, REALTIME_MODE_UDP); + } + if (realtimeOverride) return true; + + unsigned totalLen = strip.getLengthTotal(); + if (udpIn[0] == 1 && packetSize > 5) { //warls + for (size_t i = 2; i < packetSize -3; i += 4) { + setRealtimePixel(udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3], 0); + } + } else if (udpIn[0] == 2 && packetSize > 4) { //drgb + for (size_t i = 2, id = 0; i < packetSize -2 && id < totalLen; i += 3, id++) + { + setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); + } + } else if (udpIn[0] == 3 && packetSize > 6) { //drgbw + for (size_t i = 2, id = 0; i < packetSize -3 && id < totalLen; i += 4, id++) { + setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]); + } + } else if (udpIn[0] == 4 && packetSize > 7) { //dnrgb + unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); + for (size_t i = 4; i < packetSize -2 && id < totalLen; i += 3, id++) { + setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); + } + } else if (udpIn[0] == 5 && packetSize > 8) { //dnrgbw + unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); + for (size_t i = 4; i < packetSize -2 && id < totalLen; i += 4, id++) { + setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]); + } + } + if (useMainSegmentOnly) strip.trigger(); + else strip.show(); + return true; + } + + return false; +} diff --git a/wled00/sync.h b/wled00/sync.h new file mode 100644 index 0000000000..d5f13370be --- /dev/null +++ b/wled00/sync.h @@ -0,0 +1,21 @@ +/* + * Shared constants for the WLED Notifier sync protocol and its ESP-NOW transport. + * Used by sync_notifier.cpp, espnow_sync.cpp, realtime_udp.cpp and udp.cpp. + */ +#ifndef WLED_SYNC_H +#define WLED_SYNC_H + +#define UDP_SEG_SIZE 36 +#define SEG_OFFSET (41) +static constexpr size_t WLEDPACKETSIZE = 41+(WS2812FX::getMaxSegments()*UDP_SEG_SIZE); // make sure this is known at compile-time +#define UDP_IN_MAXSIZE 1472 +#define PRESUMED_NETWORK_DELAY 3 //how many ms could it take on avg to reach the receiver? This will be added to transmitted times + +typedef struct PartialEspNowPacket { + uint8_t magic; + uint8_t packet; + uint8_t noOfPackets; + uint8_t data[247]; +} partial_packet_t; + +#endif diff --git a/wled00/sync_nodes.cpp b/wled00/sync_nodes.cpp new file mode 100644 index 0000000000..55be843f29 --- /dev/null +++ b/wled00/sync_nodes.cpp @@ -0,0 +1,106 @@ +#include "wled.h" + +/* + * WLED node discovery: lightweight "which other WLED units are on the network" protocol, + * broadcast on the secondary notifier socket. Unrelated to the main state-sync protocol + * in sync_notifier.cpp. + */ + +// returns true if the packet was a node-info packet (whether or not it was actually processed) +bool parseNodeInfoPacket(const uint8_t *udpIn, unsigned len, bool isSupp, const IPAddress &localIP) +{ + if (!(isSupp && udpIn[0] == 255 && udpIn[1] == 1 && len >= 40)) return false; + + if (!nodeListEnabled || notifier2Udp.remoteIP() == localIP) return true; + + unsigned unit = udpIn[39]; + NodesMap::iterator it = Nodes.find(unit); + if (it == Nodes.end() && Nodes.size() < WLED_MAX_NODES) { // Create a new element when not present + Nodes[unit].age = 0; + it = Nodes.find(unit); + } + + if (it != Nodes.end()) { + for (size_t x = 0; x < 4; x++) { + it->second.ip[x] = udpIn[x + 2]; + } + it->second.age = 0; // reset 'age counter' + char tmpNodeName[33] = { 0 }; + memcpy(&tmpNodeName[0], reinterpret_cast(&udpIn[6]), 32); + tmpNodeName[32] = 0; + it->second.nodeName = tmpNodeName; + it->second.nodeName.trim(); + it->second.nodeType = udpIn[38]; + uint32_t build = 0; + if (len >= 44) + for (size_t i=0; isecond.build = build; + } + return true; +} + +/*********************************************************************************************\ + Refresh aging for remote units, drop if too old... +\*********************************************************************************************/ +void refreshNodeList() +{ + for (NodesMap::iterator it = Nodes.begin(); it != Nodes.end();) { + bool mustRemove = true; + + if (it->second.ip[0] != 0) { + if (it->second.age < 10) { + it->second.age++; + mustRemove = false; + ++it; + } + } + + if (mustRemove) { + it = Nodes.erase(it); + } + } +} + +/*********************************************************************************************\ + Broadcast system info to other nodes. (to update node lists) +\*********************************************************************************************/ +void sendSysInfoUDP() +{ + if (!udp2Connected) return; + + IPAddress ip = WLEDNetwork.localIP(); + if (!ip || ip == IPAddress(255,255,255,255)) ip = IPAddress(4,3,2,1); + + // TODO: make a nice struct of it and clean up + // 0: 1 byte 'binary token 255' + // 1: 1 byte id '1' + // 2: 4 byte ip + // 6: 32 char name + // 38: 1 byte node type id + // 39: 1 byte node id + // 40: 4 byte version ID + // 44 bytes total + + // send my info to the world... + uint8_t data[44] = {0}; + data[0] = 255; + data[1] = 1; + + for (size_t x = 0; x < 4; x++) { + data[x + 2] = ip[x]; + } + memcpy((byte *)data + 6, serverDescription, 32); + data[38] = uint8_t(WLED_BOARD); // see wled_boards.h + if (bri) data[38] |= 0x80U; // add on/off state + data[39] = ip[3]; // unit ID == last IP number + + uint32_t build = VERSION; + for (size_t i=0; i>(8*i)) & 0xFF; + + IPAddress broadcastIP(255, 255, 255, 255); + notifier2Udp.beginPacket(broadcastIP, udpPort2); + notifier2Udp.write(data, sizeof(data)); + notifier2Udp.endPacket(); +} diff --git a/wled00/sync_notifier.cpp b/wled00/sync_notifier.cpp new file mode 100644 index 0000000000..ed87a4f176 --- /dev/null +++ b/wled00/sync_notifier.cpp @@ -0,0 +1,414 @@ +#include "wled.h" +#include "sync.h" + +/* + * WLED's own proprietary state-sync protocol ("Notifier"): notify() builds and sends a + * packet describing this unit's current state; parseNotifyPacket() decodes an incoming + * packet and applies it. parseNotifyPacket() is transport-agnostic - it is called both + * from the UDP receive path (udp.cpp) and the ESP-NOW receive path (espnow_sync.cpp). + */ + +// notify()'s own send-retry bookkeeping. Private to this file: udp.cpp's dispatcher +// only calls notifyRetryIfNeeded(), it never touches these fields directly. +struct NotifierSendState { + byte count = 0; + byte lastCallMode = CALL_MODE_INIT; + unsigned long lastSentTime = 0; +}; +static NotifierSendState notifierSend; + +void notify(byte callMode, bool followUp) +{ +#ifndef WLED_DISABLE_ESPNOW + if (!udpConnected && !useESPNowSync) return; +#else + if (!udpConnected) return; +#endif + if (!syncGroups || !sendNotificationsRT) return; + switch (callMode) + { + case CALL_MODE_INIT: return; + case CALL_MODE_DIRECT_CHANGE: if (!notifyDirect) return; break; + case CALL_MODE_BUTTON: if (!notifyButton) return; break; + case CALL_MODE_BUTTON_PRESET: if (!notifyButton) return; break; + case CALL_MODE_NIGHTLIGHT: if (!notifyDirect) return; break; + case CALL_MODE_HUE: if (!notifyHue) return; break; + case CALL_MODE_PRESET_CYCLE: if (!notifyDirect) return; break; + case CALL_MODE_ALEXA: if (!notifyAlexa) return; break; + default: return; + } + byte udpOut[WLEDPACKETSIZE]; //TODO: optimize size to use only active segments + Segment& mainseg = strip.getMainSegment(); + udpOut[0] = 0; //0: wled notifier protocol 1: WARLS protocol + udpOut[1] = callMode; + udpOut[2] = bri; + uint32_t col = mainseg.colors[0]; + udpOut[3] = R(col); + udpOut[4] = G(col); + udpOut[5] = B(col); + udpOut[6] = nightlightActive; + udpOut[7] = nightlightDelayMins; + udpOut[8] = mainseg.mode; + udpOut[9] = mainseg.speed; + udpOut[10] = W(col); + //compatibilityVersionByte: + //0: old 1: supports white 2: supports secondary color + //3: supports FX intensity, 24 byte packet 4: supports transitionDelay 5: sup palette + //6: supports timebase syncing, 29 byte packet 7: supports tertiary color 8: supports sys time sync, 36 byte packet + //9: supports sync groups, 37 byte packet 10: supports CCT, 39 byte packet 11: per segment options, variable packet length (40+WS2812FX::getMaxSegments()*3) + //12: enhanced effect sliders, 2D & mapping options + udpOut[11] = 12; + col = mainseg.colors[1]; + udpOut[12] = R(col); + udpOut[13] = G(col); + udpOut[14] = B(col); + udpOut[15] = W(col); + udpOut[16] = mainseg.intensity; + udpOut[17] = (transitionDelay >> 0) & 0xFF; + udpOut[18] = (transitionDelay >> 8) & 0xFF; + udpOut[19] = mainseg.palette; + col = mainseg.colors[2]; + udpOut[20] = R(col); + udpOut[21] = G(col); + udpOut[22] = B(col); + udpOut[23] = W(col); + + udpOut[24] = followUp; + uint32_t t = millis() + strip.timebase; + udpOut[25] = (t >> 24) & 0xFF; + udpOut[26] = (t >> 16) & 0xFF; + udpOut[27] = (t >> 8) & 0xFF; + udpOut[28] = (t >> 0) & 0xFF; + + //sync system time + udpOut[29] = toki.getTimeSource(); + Toki::Time tm = toki.getTime(); + uint32_t unix = tm.sec; + udpOut[30] = (unix >> 24) & 0xFF; + udpOut[31] = (unix >> 16) & 0xFF; + udpOut[32] = (unix >> 8) & 0xFF; + udpOut[33] = (unix >> 0) & 0xFF; + uint16_t ms = tm.ms; + udpOut[34] = (ms >> 8) & 0xFF; + udpOut[35] = (ms >> 0) & 0xFF; + + //sync groups + udpOut[36] = syncGroups; + + //Might be changed to Kelvin in the future, receiver code should handle that case + //0: byte 38 contains 0-255 value, 255: no valid CCT, 1-254: Kelvin value MSB + udpOut[37] = strip.hasCCTBus() ? 0 : 255; //check this is 0 for the next value to be significant + udpOut[38] = mainseg.cct; + + udpOut[39] = strip.getActiveSegmentsNum(); + udpOut[40] = UDP_SEG_SIZE; //size of each loop iteration (one segment) + size_t s = 0, nsegs = strip.getSegmentsNum(); + for (size_t i = 0; i < nsegs; i++) { + const Segment &selseg = strip.getSegment(i); + if (!selseg.isActive()) continue; + unsigned ofs = 41 + s*UDP_SEG_SIZE; //start of segment offset byte + udpOut[0 +ofs] = s; + udpOut[1 +ofs] = selseg.start >> 8; + udpOut[2 +ofs] = selseg.start & 0xFF; + udpOut[3 +ofs] = selseg.stop >> 8; + udpOut[4 +ofs] = selseg.stop & 0xFF; + udpOut[5 +ofs] = selseg.grouping; + udpOut[6 +ofs] = selseg.spacing; + udpOut[7 +ofs] = selseg.offset >> 8; + udpOut[8 +ofs] = selseg.offset & 0xFF; + udpOut[9 +ofs] = selseg.options & 0x8F; //only take into account selected, mirrored, on, reversed, reverse_y (for 2D); ignore freeze, reset, transitional + udpOut[10+ofs] = selseg.opacity; + udpOut[11+ofs] = selseg.mode; + udpOut[12+ofs] = selseg.speed; + udpOut[13+ofs] = selseg.intensity; + udpOut[14+ofs] = selseg.palette; + udpOut[15+ofs] = R(selseg.colors[0]); + udpOut[16+ofs] = G(selseg.colors[0]); + udpOut[17+ofs] = B(selseg.colors[0]); + udpOut[18+ofs] = W(selseg.colors[0]); + udpOut[19+ofs] = R(selseg.colors[1]); + udpOut[20+ofs] = G(selseg.colors[1]); + udpOut[21+ofs] = B(selseg.colors[1]); + udpOut[22+ofs] = W(selseg.colors[1]); + udpOut[23+ofs] = R(selseg.colors[2]); + udpOut[24+ofs] = G(selseg.colors[2]); + udpOut[25+ofs] = B(selseg.colors[2]); + udpOut[26+ofs] = W(selseg.colors[2]); + udpOut[27+ofs] = selseg.cct; + udpOut[28+ofs] = (selseg.options>>8) & 0xFF; //mirror_y, transpose, 2D mapping & sound + udpOut[29+ofs] = selseg.custom1; + udpOut[30+ofs] = selseg.custom2; + udpOut[31+ofs] = selseg.custom3 | (selseg.check1<<5) | (selseg.check2<<6) | (selseg.check3<<7); + udpOut[32+ofs] = selseg.startY >> 8; // ATM always 0 as Segment::startY is 8-bit + udpOut[33+ofs] = selseg.startY & 0xFF; + udpOut[34+ofs] = selseg.stopY >> 8; // ATM always 0 as Segment::stopY is 8-bit + udpOut[35+ofs] = selseg.stopY & 0xFF; + ++s; + } + + //uint16_t offs = SEG_OFFSET; + //next value to be added has index: udpOut[offs + 0] + +#ifndef WLED_DISABLE_ESPNOW + if (enableESPNow && useESPNowSync && statusESPNow == ESP_NOW_STATE_ON) { + partial_packet_t buffer = {'W', 0, 1, {0}}; + // send global data + DEBUG_PRINTLN(F("ESP-NOW sending first packet.")); + const size_t bufferSize = sizeof(buffer.data)/sizeof(uint8_t); + size_t packetSize = 41; + size_t s0 = 0; + memcpy(buffer.data, udpOut, packetSize); + // stuff as many segments in first packet as possible (normally up to 5) + for (size_t i = 0; packetSize < bufferSize && i < s; i++) { + memcpy(buffer.data + packetSize, &udpOut[41+i*UDP_SEG_SIZE], UDP_SEG_SIZE); + packetSize += UDP_SEG_SIZE; + s0++; + } + if (s > s0) buffer.noOfPackets += 1 + ((s - s0) * UDP_SEG_SIZE) / bufferSize; // set number of packets + auto err = wled::espNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast(&buffer), packetSize+3); + if (!err && s0 < s) { + // send rest of the segments + buffer.packet++; + packetSize = 0; + for (size_t i = s0; i < s; i++) { + memcpy(buffer.data + packetSize, &udpOut[41+i*UDP_SEG_SIZE], UDP_SEG_SIZE); + packetSize += UDP_SEG_SIZE; + if (packetSize + UDP_SEG_SIZE < bufferSize) continue; + DEBUG_PRINTF_P(PSTR("ESP-NOW sending packet: %d (%u)\n"), (int)buffer.packet, packetSize+3); + err = wled::espNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast(&buffer), packetSize+3); + buffer.packet++; + packetSize = 0; + if (err) break; + } + if (!err && packetSize > 0) { + DEBUG_PRINTF_P(PSTR("ESP-NOW sending last packet: %d (%d)\n"), (int)buffer.packet, packetSize+3); + err = wled::espNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast(&buffer), packetSize+3); + } + } + if (err) { + DEBUG_PRINTLN(F("ESP-NOW sending packet failed.")); + } + } + if (udpConnected) +#endif + { + DEBUG_PRINTLN(F("UDP sending packet.")); + IPAddress broadcastIp = ~uint32_t(WLEDNetwork.subnetMask()) | uint32_t(WLEDNetwork.gatewayIP()); + notifierUdp.beginPacket(broadcastIp, udpPort); + notifierUdp.write(udpOut, WLEDPACKETSIZE); // TODO: add actual used buffer size + notifierUdp.endPacket(); + } + notifierSend.lastCallMode = callMode; + notifierSend.lastSentTime = millis(); + notifierSend.count = followUp ? notifierSend.count + 1 : 0; +} + +// called from udp.cpp's handleNotifications() once per loop; encapsulates the +// send-retry check so the dispatcher doesn't need to know notify()'s internal state. +void notifyRetryIfNeeded() +{ + if (udpConnected && (notifierSend.count < udpNumRetries) && ((millis() - notifierSend.lastSentTime) > 250)) { + notify(notifierSend.lastCallMode, true); + } +} + +void parseNotifyPacket(const uint8_t *udpIn) { + //ignore notification if received within a second after sending a notification ourselves + if (millis() - notifierSend.lastSentTime < 1000) return; + if (udpIn[1] > 199) return; //do not receive custom versions + + //compatibilityVersionByte: + byte version = udpIn[11]; + DEBUG_PRINTF_P(PSTR("UDP packet version: %d\n"), (int)version); + + // if we are not part of any sync group ignore message + if (version < 9) { + // legacy senders are treated as if sending in sync group 1 only + if (!(receiveGroups & 0x01)) return; + } else if (!(receiveGroups & udpIn[36])) return; + + bool someSel = (receiveNotificationBrightness || receiveNotificationColor || receiveNotificationEffects || receiveNotificationPalette); + + // set transition time before making any segment changes + if (version > 3) { + jsonTransitionOnce = true; + strip.setTransition(((udpIn[17] << 0) & 0xFF) + ((udpIn[18] << 8) & 0xFF00)); + } + + //apply colors from notification to main segment, only if not syncing full segments + if ((receiveNotificationColor || !someSel) && (version < 11 || !receiveSegmentOptions)) { + // primary color, only apply white if intented (version > 0) + strip.getMainSegment().setColor(0, RGBW32(udpIn[3], udpIn[4], udpIn[5], (version > 0) ? udpIn[10] : 0)); + if (version > 1) { + strip.getMainSegment().setColor(1, RGBW32(udpIn[12], udpIn[13], udpIn[14], udpIn[15])); // secondary color + } + if (version > 6) { + strip.getMainSegment().setColor(2, RGBW32(udpIn[20], udpIn[21], udpIn[22], udpIn[23])); // tertiary color + if (version > 9 && udpIn[37] < 255) { // valid CCT/Kelvin value + unsigned cct = udpIn[38]; + if (udpIn[37] > 0) { //Kelvin + cct |= (udpIn[37] << 8); + } + strip.setCCT(cct); + } + } + } + + bool timebaseUpdated = false; + //apply effects from notification + bool applyEffects = (receiveNotificationEffects || !someSel); + if (applyEffects && currentPlaylist >= 0) unloadPlaylist(); + if (version > 10 && (receiveSegmentOptions || receiveSegmentBounds)) { + unsigned numSrcSegs = udpIn[39]; + DEBUG_PRINTF_P(PSTR("UDP segments: %d\n"), numSrcSegs); + // are we syncing bounds and slave has more active segments than master? + if (receiveSegmentBounds && numSrcSegs < strip.getActiveSegmentsNum()) { + DEBUG_PRINTLN(F("Removing excessive segments.")); + strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" + for (size_t i=strip.getSegmentsNum(); i>numSrcSegs && i>0; i--) { + Segment &seg = strip.getSegment(i-1); + if (seg.isActive()) seg.deactivate(); // delete segment + } + strip.resume(); + } + size_t inactiveSegs = 0; + for (size_t i = 0; i < numSrcSegs && i < WS2812FX::getMaxSegments(); i++) { + unsigned ofs = 41 + i*udpIn[40]; //start of segment offset byte + if (ofs + 36 > UDP_IN_MAXSIZE) break; // avoid reading outside of array + unsigned id = udpIn[0 +ofs]; + DEBUG_PRINTF_P(PSTR("UDP segment received: %u\n"), id); + if (id > strip.getSegmentsNum()) break; + else if (id == strip.getSegmentsNum()) { + if (receiveSegmentBounds && id < WS2812FX::getMaxSegments()) strip.appendSegment(); + else break; + } + DEBUG_PRINTF_P(PSTR("UDP segment check: %u\n"), id); + Segment& selseg = strip.getSegment(id); + // if we are not syncing bounds skip unselected segments + if (selseg.isActive() && !(selseg.isSelected() || receiveSegmentBounds)) continue; + // ignore segment if it is inactive and we are not syncing bounds + if (!receiveSegmentBounds) { + if (!selseg.isActive()) { + inactiveSegs++; + DEBUG_PRINTLN(F("Inactive segment.")); + continue; + } else { + id += inactiveSegs; // adjust id + } + } + DEBUG_PRINTF_P(PSTR("UDP segment processing: %u\n"), id); + + uint16_t start = (udpIn[1+ofs] << 8 | udpIn[2+ofs]); + uint16_t stop = (udpIn[3+ofs] << 8 | udpIn[4+ofs]); + uint16_t startY = version > 11 ? (udpIn[32+ofs] << 8 | udpIn[33+ofs]) : 0; + uint16_t stopY = version > 11 ? (udpIn[34+ofs] << 8 | udpIn[35+ofs]) : 1; + uint16_t offset = (udpIn[7+ofs] << 8 | udpIn[8+ofs]); + if (!receiveSegmentOptions) { + DEBUG_PRINTF_P(PSTR("Set segment w/o options: %d [%d,%d;%d,%d]\n"), id, (int)start, (int)stop, (int)startY, (int)stopY); + strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" + selseg.setGeometry(start, stop, selseg.grouping, selseg.spacing, offset, startY, stopY, selseg.map1D2D); + strip.resume(); + continue; // we do receive bounds, but not options + } + selseg.options = (selseg.options & 0x0071U) | (udpIn[9 +ofs] & 0x0E); // ignore selected, freeze, reset & transitional + selseg.setOpacity(udpIn[10+ofs]); + if (applyEffects) { + DEBUG_PRINTF_P(PSTR("Apply effect: %u\n"), id); + selseg.setMode(udpIn[11+ofs]); + selseg.speed = udpIn[12+ofs]; + selseg.intensity = udpIn[13+ofs]; + } + if (receiveNotificationPalette || !someSel) { + DEBUG_PRINTF_P(PSTR("Apply palette: %u\n"), id); + selseg.palette = udpIn[14+ofs]; + } + if (receiveNotificationColor || !someSel) { + DEBUG_PRINTF_P(PSTR("Apply color: %u\n"), id); + selseg.setColor(0, RGBW32(udpIn[15+ofs],udpIn[16+ofs],udpIn[17+ofs],udpIn[18+ofs])); + selseg.setColor(1, RGBW32(udpIn[19+ofs],udpIn[20+ofs],udpIn[21+ofs],udpIn[22+ofs])); + selseg.setColor(2, RGBW32(udpIn[23+ofs],udpIn[24+ofs],udpIn[25+ofs],udpIn[26+ofs])); + selseg.setCCT(udpIn[27+ofs]); + } + if (version > 11) { + // when applying synced options ignore selected as it may be used as indicator of which segments to sync + // freeze, reset should never be synced + // LSB to MSB: select, reverse, on, mirror, freeze, reset, reverse_y, mirror_y, transpose, map1d2d (3), ssim (2), set (2) + DEBUG_PRINTF_P(PSTR("Apply options: %u\n"), id); + selseg.options = (selseg.options & 0b0000000000110001U) | ((uint16_t)udpIn[28+ofs]<<8) | (udpIn[9 +ofs] & 0b11001110U); // ignore selected, freeze, reset + if (applyEffects) { + DEBUG_PRINTF_P(PSTR("Apply sliders: %u\n"), id); + selseg.custom1 = udpIn[29+ofs]; + selseg.custom2 = udpIn[30+ofs]; + selseg.custom3 = udpIn[31+ofs] & 0x1F; + selseg.check1 = (udpIn[31+ofs]>>5) & 0x1; + selseg.check2 = (udpIn[31+ofs]>>6) & 0x1; + selseg.check3 = (udpIn[31+ofs]>>7) & 0x1; + } + } + if (receiveSegmentBounds) { + DEBUG_PRINTF_P(PSTR("Set segment w/ options: %d [%d,%d;%d,%d]\n"), id, (int)start, (int)stop, (int)startY, (int)stopY); + strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" + selseg.setGeometry(start, stop, udpIn[5+ofs], udpIn[6+ofs], offset, startY, stopY, selseg.map1D2D); + strip.resume(); + } else { + DEBUG_PRINTF_P(PSTR("Set segment grouping: %d [%d,%d]\n"), id, (int)udpIn[5+ofs], (int)udpIn[6+ofs]); + strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" + selseg.setGeometry(selseg.start, selseg.stop, udpIn[5+ofs], udpIn[6+ofs], selseg.offset, selseg.startY, selseg.stopY, selseg.map1D2D); + strip.resume(); + } + } + stateChanged = true; + } + + // simple effect sync, applies to all selected segments + if ((applyEffects || receiveNotificationPalette) && (version < 11 || !receiveSegmentOptions)) { + for (size_t i = 0; i < strip.getSegmentsNum(); i++) { + Segment& seg = strip.getSegment(i); + if (!seg.isActive() || !seg.isSelected()) continue; + if (applyEffects) { + seg.setMode(udpIn[8]); + seg.speed = udpIn[9]; + if (version > 2) seg.intensity = udpIn[16]; + } + if (version > 4 && receiveNotificationPalette) seg.setPalette(udpIn[19]); + } + stateChanged = true; + } + + if (applyEffects && version > 5) { + uint32_t t = (udpIn[25] << 24) | (udpIn[26] << 16) | (udpIn[27] << 8) | (udpIn[28]); + t += PRESUMED_NETWORK_DELAY; //adjust trivially for network delay + t -= millis(); + strip.timebase = t; + timebaseUpdated = true; + } + + //adjust system time, but only if sender is more accurate than self + if (version > 7) { + Toki::Time tm; + tm.sec = (udpIn[30] << 24) | (udpIn[31] << 16) | (udpIn[32] << 8) | (udpIn[33]); + tm.ms = (udpIn[34] << 8) | (udpIn[35]); + if (udpIn[29] > toki.getTimeSource()) { //if sender's time source is more accurate + toki.adjust(tm, PRESUMED_NETWORK_DELAY); //adjust trivially for network delay + uint8_t ts = TOKI_TS_UDP; + if (udpIn[29] > 99) ts = TOKI_TS_UDP_NTP; + else if (udpIn[29] >= TOKI_TS_SEC) ts = TOKI_TS_UDP_SEC; + toki.setTime(tm, ts); + } else if (timebaseUpdated && toki.getTimeSource() > 99) { //if we both have good times, get a more accurate timebase + Toki::Time myTime = toki.getTime(); + uint32_t diff = toki.msDifference(tm, myTime); + strip.timebase -= PRESUMED_NETWORK_DELAY; //no need to presume, use difference between NTP times at send and receive points + if (toki.isLater(tm, myTime)) { + strip.timebase += diff; + } else { + strip.timebase -= diff; + } + } + } + + nightlightActive = udpIn[6]; + if (nightlightActive) nightlightDelayMins = udpIn[7]; + + if (receiveNotificationBrightness || !someSel) bri = udpIn[2]; + stateUpdated(CALL_MODE_NOTIFICATION); +} diff --git a/wled00/udp.cpp b/wled00/udp.cpp index ac5554df40..b733db585b 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -1,476 +1,25 @@ #include "wled.h" +#include "sync.h" /* - * UDP sync notifier / Realtime / Hyperion / TPM2.NET + * UDP sync dispatcher. + * + * The individual protocols this dispatches to now live in their own files: + * - sync_notifier.cpp WLED's own state-sync protocol (notify/parseNotifyPacket) + * - sync_nodes.cpp node discovery + * - realtime.cpp shared realtime state machine + * - realtime_udp.cpp Hyperion / TPM2.NET / legacy UDP realtime formats + * - realtime_broadcast.cpp outbound Art-Net/DDP/E1.31 sender (see e131.cpp for the + * inbound receiver side of those same wire protocols) + * - espnow_sync.cpp ESP-NOW transport for the notifier protocol */ -#define UDP_SEG_SIZE 36 -#define SEG_OFFSET (41) -static constexpr size_t WLEDPACKETSIZE = 41+(WS2812FX::getMaxSegments()*UDP_SEG_SIZE); // make sure this is known at compile-time -#define UDP_IN_MAXSIZE 1472 -#define PRESUMED_NETWORK_DELAY 3 //how many ms could it take on avg to reach the receiver? This will be added to transmitted times - -typedef struct PartialEspNowPacket { - uint8_t magic; - uint8_t packet; - uint8_t noOfPackets; - uint8_t data[247]; -} partial_packet_t; - -void notify(byte callMode, bool followUp) -{ -#ifndef WLED_DISABLE_ESPNOW - if (!udpConnected && !useESPNowSync) return; -#else - if (!udpConnected) return; -#endif - if (!syncGroups || !sendNotificationsRT) return; - switch (callMode) - { - case CALL_MODE_INIT: return; - case CALL_MODE_DIRECT_CHANGE: if (!notifyDirect) return; break; - case CALL_MODE_BUTTON: if (!notifyButton) return; break; - case CALL_MODE_BUTTON_PRESET: if (!notifyButton) return; break; - case CALL_MODE_NIGHTLIGHT: if (!notifyDirect) return; break; - case CALL_MODE_HUE: if (!notifyHue) return; break; - case CALL_MODE_PRESET_CYCLE: if (!notifyDirect) return; break; - case CALL_MODE_ALEXA: if (!notifyAlexa) return; break; - default: return; - } - byte udpOut[WLEDPACKETSIZE]; //TODO: optimize size to use only active segments - Segment& mainseg = strip.getMainSegment(); - udpOut[0] = 0; //0: wled notifier protocol 1: WARLS protocol - udpOut[1] = callMode; - udpOut[2] = bri; - uint32_t col = mainseg.colors[0]; - udpOut[3] = R(col); - udpOut[4] = G(col); - udpOut[5] = B(col); - udpOut[6] = nightlightActive; - udpOut[7] = nightlightDelayMins; - udpOut[8] = mainseg.mode; - udpOut[9] = mainseg.speed; - udpOut[10] = W(col); - //compatibilityVersionByte: - //0: old 1: supports white 2: supports secondary color - //3: supports FX intensity, 24 byte packet 4: supports transitionDelay 5: sup palette - //6: supports timebase syncing, 29 byte packet 7: supports tertiary color 8: supports sys time sync, 36 byte packet - //9: supports sync groups, 37 byte packet 10: supports CCT, 39 byte packet 11: per segment options, variable packet length (40+WS2812FX::getMaxSegments()*3) - //12: enhanced effect sliders, 2D & mapping options - udpOut[11] = 12; - col = mainseg.colors[1]; - udpOut[12] = R(col); - udpOut[13] = G(col); - udpOut[14] = B(col); - udpOut[15] = W(col); - udpOut[16] = mainseg.intensity; - udpOut[17] = (transitionDelay >> 0) & 0xFF; - udpOut[18] = (transitionDelay >> 8) & 0xFF; - udpOut[19] = mainseg.palette; - col = mainseg.colors[2]; - udpOut[20] = R(col); - udpOut[21] = G(col); - udpOut[22] = B(col); - udpOut[23] = W(col); - - udpOut[24] = followUp; - uint32_t t = millis() + strip.timebase; - udpOut[25] = (t >> 24) & 0xFF; - udpOut[26] = (t >> 16) & 0xFF; - udpOut[27] = (t >> 8) & 0xFF; - udpOut[28] = (t >> 0) & 0xFF; - - //sync system time - udpOut[29] = toki.getTimeSource(); - Toki::Time tm = toki.getTime(); - uint32_t unix = tm.sec; - udpOut[30] = (unix >> 24) & 0xFF; - udpOut[31] = (unix >> 16) & 0xFF; - udpOut[32] = (unix >> 8) & 0xFF; - udpOut[33] = (unix >> 0) & 0xFF; - uint16_t ms = tm.ms; - udpOut[34] = (ms >> 8) & 0xFF; - udpOut[35] = (ms >> 0) & 0xFF; - - //sync groups - udpOut[36] = syncGroups; - - //Might be changed to Kelvin in the future, receiver code should handle that case - //0: byte 38 contains 0-255 value, 255: no valid CCT, 1-254: Kelvin value MSB - udpOut[37] = strip.hasCCTBus() ? 0 : 255; //check this is 0 for the next value to be significant - udpOut[38] = mainseg.cct; - - udpOut[39] = strip.getActiveSegmentsNum(); - udpOut[40] = UDP_SEG_SIZE; //size of each loop iteration (one segment) - size_t s = 0, nsegs = strip.getSegmentsNum(); - for (size_t i = 0; i < nsegs; i++) { - const Segment &selseg = strip.getSegment(i); - if (!selseg.isActive()) continue; - unsigned ofs = 41 + s*UDP_SEG_SIZE; //start of segment offset byte - udpOut[0 +ofs] = s; - udpOut[1 +ofs] = selseg.start >> 8; - udpOut[2 +ofs] = selseg.start & 0xFF; - udpOut[3 +ofs] = selseg.stop >> 8; - udpOut[4 +ofs] = selseg.stop & 0xFF; - udpOut[5 +ofs] = selseg.grouping; - udpOut[6 +ofs] = selseg.spacing; - udpOut[7 +ofs] = selseg.offset >> 8; - udpOut[8 +ofs] = selseg.offset & 0xFF; - udpOut[9 +ofs] = selseg.options & 0x8F; //only take into account selected, mirrored, on, reversed, reverse_y (for 2D); ignore freeze, reset, transitional - udpOut[10+ofs] = selseg.opacity; - udpOut[11+ofs] = selseg.mode; - udpOut[12+ofs] = selseg.speed; - udpOut[13+ofs] = selseg.intensity; - udpOut[14+ofs] = selseg.palette; - udpOut[15+ofs] = R(selseg.colors[0]); - udpOut[16+ofs] = G(selseg.colors[0]); - udpOut[17+ofs] = B(selseg.colors[0]); - udpOut[18+ofs] = W(selseg.colors[0]); - udpOut[19+ofs] = R(selseg.colors[1]); - udpOut[20+ofs] = G(selseg.colors[1]); - udpOut[21+ofs] = B(selseg.colors[1]); - udpOut[22+ofs] = W(selseg.colors[1]); - udpOut[23+ofs] = R(selseg.colors[2]); - udpOut[24+ofs] = G(selseg.colors[2]); - udpOut[25+ofs] = B(selseg.colors[2]); - udpOut[26+ofs] = W(selseg.colors[2]); - udpOut[27+ofs] = selseg.cct; - udpOut[28+ofs] = (selseg.options>>8) & 0xFF; //mirror_y, transpose, 2D mapping & sound - udpOut[29+ofs] = selseg.custom1; - udpOut[30+ofs] = selseg.custom2; - udpOut[31+ofs] = selseg.custom3 | (selseg.check1<<5) | (selseg.check2<<6) | (selseg.check3<<7); - udpOut[32+ofs] = selseg.startY >> 8; // ATM always 0 as Segment::startY is 8-bit - udpOut[33+ofs] = selseg.startY & 0xFF; - udpOut[34+ofs] = selseg.stopY >> 8; // ATM always 0 as Segment::stopY is 8-bit - udpOut[35+ofs] = selseg.stopY & 0xFF; - ++s; - } - - //uint16_t offs = SEG_OFFSET; - //next value to be added has index: udpOut[offs + 0] - -#ifndef WLED_DISABLE_ESPNOW - if (enableESPNow && useESPNowSync && statusESPNow == ESP_NOW_STATE_ON) { - partial_packet_t buffer = {'W', 0, 1, {0}}; - // send global data - DEBUG_PRINTLN(F("ESP-NOW sending first packet.")); - const size_t bufferSize = sizeof(buffer.data)/sizeof(uint8_t); - size_t packetSize = 41; - size_t s0 = 0; - memcpy(buffer.data, udpOut, packetSize); - // stuff as many segments in first packet as possible (normally up to 5) - for (size_t i = 0; packetSize < bufferSize && i < s; i++) { - memcpy(buffer.data + packetSize, &udpOut[41+i*UDP_SEG_SIZE], UDP_SEG_SIZE); - packetSize += UDP_SEG_SIZE; - s0++; - } - if (s > s0) buffer.noOfPackets += 1 + ((s - s0) * UDP_SEG_SIZE) / bufferSize; // set number of packets - auto err = wled::espNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast(&buffer), packetSize+3); - if (!err && s0 < s) { - // send rest of the segments - buffer.packet++; - packetSize = 0; - for (size_t i = s0; i < s; i++) { - memcpy(buffer.data + packetSize, &udpOut[41+i*UDP_SEG_SIZE], UDP_SEG_SIZE); - packetSize += UDP_SEG_SIZE; - if (packetSize + UDP_SEG_SIZE < bufferSize) continue; - DEBUG_PRINTF_P(PSTR("ESP-NOW sending packet: %d (%u)\n"), (int)buffer.packet, packetSize+3); - err = wled::espNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast(&buffer), packetSize+3); - buffer.packet++; - packetSize = 0; - if (err) break; - } - if (!err && packetSize > 0) { - DEBUG_PRINTF_P(PSTR("ESP-NOW sending last packet: %d (%d)\n"), (int)buffer.packet, packetSize+3); - err = wled::espNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast(&buffer), packetSize+3); - } - } - if (err) { - DEBUG_PRINTLN(F("ESP-NOW sending packet failed.")); - } - } - if (udpConnected) -#endif - { - DEBUG_PRINTLN(F("UDP sending packet.")); - IPAddress broadcastIp = ~uint32_t(WLEDNetwork.subnetMask()) | uint32_t(WLEDNetwork.gatewayIP()); - notifierUdp.beginPacket(broadcastIp, udpPort); - notifierUdp.write(udpOut, WLEDPACKETSIZE); // TODO: add actual used buffer size - notifierUdp.endPacket(); - } - notificationSentCallMode = callMode; - notificationSentTime = millis(); - notificationCount = followUp ? notificationCount + 1 : 0; -} - -static void parseNotifyPacket(const uint8_t *udpIn) { - //ignore notification if received within a second after sending a notification ourselves - if (millis() - notificationSentTime < 1000) return; - if (udpIn[1] > 199) return; //do not receive custom versions - - //compatibilityVersionByte: - byte version = udpIn[11]; - DEBUG_PRINTF_P(PSTR("UDP packet version: %d\n"), (int)version); - - // if we are not part of any sync group ignore message - if (version < 9) { - // legacy senders are treated as if sending in sync group 1 only - if (!(receiveGroups & 0x01)) return; - } else if (!(receiveGroups & udpIn[36])) return; - - bool someSel = (receiveNotificationBrightness || receiveNotificationColor || receiveNotificationEffects || receiveNotificationPalette); - - // set transition time before making any segment changes - if (version > 3) { - jsonTransitionOnce = true; - strip.setTransition(((udpIn[17] << 0) & 0xFF) + ((udpIn[18] << 8) & 0xFF00)); - } - - //apply colors from notification to main segment, only if not syncing full segments - if ((receiveNotificationColor || !someSel) && (version < 11 || !receiveSegmentOptions)) { - // primary color, only apply white if intented (version > 0) - strip.getMainSegment().setColor(0, RGBW32(udpIn[3], udpIn[4], udpIn[5], (version > 0) ? udpIn[10] : 0)); - if (version > 1) { - strip.getMainSegment().setColor(1, RGBW32(udpIn[12], udpIn[13], udpIn[14], udpIn[15])); // secondary color - } - if (version > 6) { - strip.getMainSegment().setColor(2, RGBW32(udpIn[20], udpIn[21], udpIn[22], udpIn[23])); // tertiary color - if (version > 9 && udpIn[37] < 255) { // valid CCT/Kelvin value - unsigned cct = udpIn[38]; - if (udpIn[37] > 0) { //Kelvin - cct |= (udpIn[37] << 8); - } - strip.setCCT(cct); - } - } - } - - bool timebaseUpdated = false; - //apply effects from notification - bool applyEffects = (receiveNotificationEffects || !someSel); - if (applyEffects && currentPlaylist >= 0) unloadPlaylist(); - if (version > 10 && (receiveSegmentOptions || receiveSegmentBounds)) { - unsigned numSrcSegs = udpIn[39]; - DEBUG_PRINTF_P(PSTR("UDP segments: %d\n"), numSrcSegs); - // are we syncing bounds and slave has more active segments than master? - if (receiveSegmentBounds && numSrcSegs < strip.getActiveSegmentsNum()) { - DEBUG_PRINTLN(F("Removing excessive segments.")); - strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" - for (size_t i=strip.getSegmentsNum(); i>numSrcSegs && i>0; i--) { - Segment &seg = strip.getSegment(i-1); - if (seg.isActive()) seg.deactivate(); // delete segment - } - strip.resume(); - } - size_t inactiveSegs = 0; - for (size_t i = 0; i < numSrcSegs && i < WS2812FX::getMaxSegments(); i++) { - unsigned ofs = 41 + i*udpIn[40]; //start of segment offset byte - if (ofs + 36 > UDP_IN_MAXSIZE) break; // avoid reading outside of array - unsigned id = udpIn[0 +ofs]; - DEBUG_PRINTF_P(PSTR("UDP segment received: %u\n"), id); - if (id > strip.getSegmentsNum()) break; - else if (id == strip.getSegmentsNum()) { - if (receiveSegmentBounds && id < WS2812FX::getMaxSegments()) strip.appendSegment(); - else break; - } - DEBUG_PRINTF_P(PSTR("UDP segment check: %u\n"), id); - Segment& selseg = strip.getSegment(id); - // if we are not syncing bounds skip unselected segments - if (selseg.isActive() && !(selseg.isSelected() || receiveSegmentBounds)) continue; - // ignore segment if it is inactive and we are not syncing bounds - if (!receiveSegmentBounds) { - if (!selseg.isActive()) { - inactiveSegs++; - DEBUG_PRINTLN(F("Inactive segment.")); - continue; - } else { - id += inactiveSegs; // adjust id - } - } - DEBUG_PRINTF_P(PSTR("UDP segment processing: %u\n"), id); - - uint16_t start = (udpIn[1+ofs] << 8 | udpIn[2+ofs]); - uint16_t stop = (udpIn[3+ofs] << 8 | udpIn[4+ofs]); - uint16_t startY = version > 11 ? (udpIn[32+ofs] << 8 | udpIn[33+ofs]) : 0; - uint16_t stopY = version > 11 ? (udpIn[34+ofs] << 8 | udpIn[35+ofs]) : 1; - uint16_t offset = (udpIn[7+ofs] << 8 | udpIn[8+ofs]); - if (!receiveSegmentOptions) { - DEBUG_PRINTF_P(PSTR("Set segment w/o options: %d [%d,%d;%d,%d]\n"), id, (int)start, (int)stop, (int)startY, (int)stopY); - strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" - selseg.setGeometry(start, stop, selseg.grouping, selseg.spacing, offset, startY, stopY, selseg.map1D2D); - strip.resume(); - continue; // we do receive bounds, but not options - } - selseg.options = (selseg.options & 0x0071U) | (udpIn[9 +ofs] & 0x0E); // ignore selected, freeze, reset & transitional - selseg.setOpacity(udpIn[10+ofs]); - if (applyEffects) { - DEBUG_PRINTF_P(PSTR("Apply effect: %u\n"), id); - selseg.setMode(udpIn[11+ofs]); - selseg.speed = udpIn[12+ofs]; - selseg.intensity = udpIn[13+ofs]; - } - if (receiveNotificationPalette || !someSel) { - DEBUG_PRINTF_P(PSTR("Apply palette: %u\n"), id); - selseg.palette = udpIn[14+ofs]; - } - if (receiveNotificationColor || !someSel) { - DEBUG_PRINTF_P(PSTR("Apply color: %u\n"), id); - selseg.setColor(0, RGBW32(udpIn[15+ofs],udpIn[16+ofs],udpIn[17+ofs],udpIn[18+ofs])); - selseg.setColor(1, RGBW32(udpIn[19+ofs],udpIn[20+ofs],udpIn[21+ofs],udpIn[22+ofs])); - selseg.setColor(2, RGBW32(udpIn[23+ofs],udpIn[24+ofs],udpIn[25+ofs],udpIn[26+ofs])); - selseg.setCCT(udpIn[27+ofs]); - } - if (version > 11) { - // when applying synced options ignore selected as it may be used as indicator of which segments to sync - // freeze, reset should never be synced - // LSB to MSB: select, reverse, on, mirror, freeze, reset, reverse_y, mirror_y, transpose, map1d2d (3), ssim (2), set (2) - DEBUG_PRINTF_P(PSTR("Apply options: %u\n"), id); - selseg.options = (selseg.options & 0b0000000000110001U) | ((uint16_t)udpIn[28+ofs]<<8) | (udpIn[9 +ofs] & 0b11001110U); // ignore selected, freeze, reset - if (applyEffects) { - DEBUG_PRINTF_P(PSTR("Apply sliders: %u\n"), id); - selseg.custom1 = udpIn[29+ofs]; - selseg.custom2 = udpIn[30+ofs]; - selseg.custom3 = udpIn[31+ofs] & 0x1F; - selseg.check1 = (udpIn[31+ofs]>>5) & 0x1; - selseg.check2 = (udpIn[31+ofs]>>6) & 0x1; - selseg.check3 = (udpIn[31+ofs]>>7) & 0x1; - } - } - if (receiveSegmentBounds) { - DEBUG_PRINTF_P(PSTR("Set segment w/ options: %d [%d,%d;%d,%d]\n"), id, (int)start, (int)stop, (int)startY, (int)stopY); - strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" - selseg.setGeometry(start, stop, udpIn[5+ofs], udpIn[6+ofs], offset, startY, stopY, selseg.map1D2D); - strip.resume(); - } else { - DEBUG_PRINTF_P(PSTR("Set segment grouping: %d [%d,%d]\n"), id, (int)udpIn[5+ofs], (int)udpIn[6+ofs]); - strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" - selseg.setGeometry(selseg.start, selseg.stop, udpIn[5+ofs], udpIn[6+ofs], selseg.offset, selseg.startY, selseg.stopY, selseg.map1D2D); - strip.resume(); - } - } - stateChanged = true; - } - - // simple effect sync, applies to all selected segments - if ((applyEffects || receiveNotificationPalette) && (version < 11 || !receiveSegmentOptions)) { - for (size_t i = 0; i < strip.getSegmentsNum(); i++) { - Segment& seg = strip.getSegment(i); - if (!seg.isActive() || !seg.isSelected()) continue; - if (applyEffects) { - seg.setMode(udpIn[8]); - seg.speed = udpIn[9]; - if (version > 2) seg.intensity = udpIn[16]; - } - if (version > 4 && receiveNotificationPalette) seg.setPalette(udpIn[19]); - } - stateChanged = true; - } - - if (applyEffects && version > 5) { - uint32_t t = (udpIn[25] << 24) | (udpIn[26] << 16) | (udpIn[27] << 8) | (udpIn[28]); - t += PRESUMED_NETWORK_DELAY; //adjust trivially for network delay - t -= millis(); - strip.timebase = t; - timebaseUpdated = true; - } - - //adjust system time, but only if sender is more accurate than self - if (version > 7) { - Toki::Time tm; - tm.sec = (udpIn[30] << 24) | (udpIn[31] << 16) | (udpIn[32] << 8) | (udpIn[33]); - tm.ms = (udpIn[34] << 8) | (udpIn[35]); - if (udpIn[29] > toki.getTimeSource()) { //if sender's time source is more accurate - toki.adjust(tm, PRESUMED_NETWORK_DELAY); //adjust trivially for network delay - uint8_t ts = TOKI_TS_UDP; - if (udpIn[29] > 99) ts = TOKI_TS_UDP_NTP; - else if (udpIn[29] >= TOKI_TS_SEC) ts = TOKI_TS_UDP_SEC; - toki.setTime(tm, ts); - } else if (timebaseUpdated && toki.getTimeSource() > 99) { //if we both have good times, get a more accurate timebase - Toki::Time myTime = toki.getTime(); - uint32_t diff = toki.msDifference(tm, myTime); - strip.timebase -= PRESUMED_NETWORK_DELAY; //no need to presume, use difference between NTP times at send and receive points - if (toki.isLater(tm, myTime)) { - strip.timebase += diff; - } else { - strip.timebase -= diff; - } - } - } - - nightlightActive = udpIn[6]; - if (nightlightActive) nightlightDelayMins = udpIn[7]; - - if (receiveNotificationBrightness || !someSel) bri = udpIn[2]; - stateUpdated(CALL_MODE_NOTIFICATION); -} - -// realtimeLock() is called from UDP notifications, JSON API or serial Ada -void realtimeLock(uint32_t timeoutMs, byte md) -{ - if (!realtimeMode && !realtimeOverride) { - if (useMainSegmentOnly) { - Segment& mainseg = strip.getMainSegment(); - mainseg.clear(); // clear entire segment (in case sender transmits less pixels) - mainseg.freeze = true; - // if WLED was off and using main segment only, freeze non-main segments so they stay off - if (bri == 0) { - for (size_t s = 0; s < strip.getSegmentsNum(); s++) strip.getSegment(s).freeze = true; - } - } else { - // clear entire strip - strip.fill(BLACK); - } - // if strip is off (bri==0) and not already in RTM - if (briT == 0) { - strip.setBrightness(briLast, true); - } - } - - if (realtimeTimeout != UINT32_MAX) { - realtimeTimeout = (timeoutMs == 255001 || timeoutMs == 65000) ? UINT32_MAX : millis() + timeoutMs; - } - realtimeMode = md; - - if (realtimeOverride) return; - if (arlsForceMaxBri) strip.setBrightness(255, true); - if (briT > 0 && md == REALTIME_MODE_GENERIC) strip.show(); -} - -void exitRealtime() { - if (!realtimeMode) return; - if (realtimeOverride == REALTIME_OVERRIDE_ONCE) realtimeOverride = REALTIME_OVERRIDE_NONE; - strip.setBrightness(bri, true); - realtimeTimeout = 0; // cancel realtime mode immediately - realtimeMode = REALTIME_MODE_INACTIVE; // inform UI immediately - realtimeIP[0] = 0; - if (useMainSegmentOnly) { // unfreeze live segment again - strip.getMainSegment().freeze = false; - strip.trigger(); - } else { - strip.show(); // possible fix for #3589 - } - updateInterfaces(CALL_MODE_WS_SEND); -} - - -#define TMP2NET_OUT_PORT 65442 - -static void sendTPM2Ack() { - notifierUdp.beginPacket(notifierUdp.remoteIP(), TMP2NET_OUT_PORT); - uint8_t response_ack = 0xac; - notifierUdp.write(&response_ack, 1); - notifierUdp.endPacket(); -} - - void handleNotifications() { IPAddress localIP; //send second notification if enabled - if(udpConnected && (notificationCount < udpNumRetries) && ((millis()-notificationSentTime) > 250)){ - notify(notificationSentCallMode,true); - } + notifyRetryIfNeeded(); if (e131NewData && millis() - strip.getLastShow() > 15) { @@ -494,24 +43,7 @@ void handleNotifications() //hyperion / raw RGB if (!packetSize && udpRgbConnected) { - packetSize = rgbUdp.parsePacket(); - if (packetSize) { - if (!receiveDirect) return; - if (packetSize > UDP_IN_MAXSIZE || packetSize < 3) return; // packetSize must not exceed buffersize (UDP_IN_MAXSIZE) - realtimeIP = rgbUdp.remoteIP(); - DEBUG_PRINTLN(rgbUdp.remoteIP()); - uint8_t lbuf[packetSize]; - rgbUdp.read(lbuf, packetSize); - realtimeLock(realtimeTimeoutMs, REALTIME_MODE_HYPERION); - if (realtimeOverride) return; - unsigned totalLen = strip.getLengthTotal(); - for (size_t i = 0, id = 0; i < packetSize -2 && id < totalLen; i += 3, id++) { - setRealtimePixel(id, lbuf[i], lbuf[i+1], lbuf[i+2], 0); - } - if (useMainSegmentOnly) strip.trigger(); - else strip.show(); - return; - } + if (handleHyperionPacket()) return; } localIP = WLEDNetwork.localIP(); @@ -525,35 +57,7 @@ void handleNotifications() else len = notifierUdp.read(udpIn, packetSize); // WLED nodes info notifications - if (isSupp && udpIn[0] == 255 && udpIn[1] == 1 && len >= 40) { - if (!nodeListEnabled || notifier2Udp.remoteIP() == localIP) return; - - unsigned unit = udpIn[39]; - NodesMap::iterator it = Nodes.find(unit); - if (it == Nodes.end() && Nodes.size() < WLED_MAX_NODES) { // Create a new element when not present - Nodes[unit].age = 0; - it = Nodes.find(unit); - } - - if (it != Nodes.end()) { - for (size_t x = 0; x < 4; x++) { - it->second.ip[x] = udpIn[x + 2]; - } - it->second.age = 0; // reset 'age counter' - char tmpNodeName[33] = { 0 }; - memcpy(&tmpNodeName[0], reinterpret_cast(&udpIn[6]), 32); - tmpNodeName[32] = 0; - it->second.nodeName = tmpNodeName; - it->second.nodeName.trim(); - it->second.nodeType = udpIn[38]; - uint32_t build = 0; - if (len >= 44) - for (size_t i=0; isecond.build = build; - } - return; - } + if (parseNodeInfoPacket(udpIn, len, isSupp, localIP)) return; //wled notifier, ignore if realtime packets active if (udpIn[0] == 0 && !realtimeMode && receiveGroups) @@ -564,83 +68,7 @@ void handleNotifications() } if (receiveDirect) { - //TPM2.NET - if (udpIn[0] == 0x9c) { - //WARNING: this code assumes that the final TMP2.NET payload is evenly distributed if using multiple packets (ie. frame size is constant) - //if the number of LEDs in your installation doesn't allow that, please include padding bytes at the end of the last packet - byte tpmType = udpIn[1]; - if (tpmType == 0xaa) { //TPM2.NET polling, expect answer - sendTPM2Ack(); return; - } - if (tpmType != 0xda) return; //return if notTPM2.NET data - - realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP(); - realtimeLock(realtimeTimeoutMs, REALTIME_MODE_TPM2NET); - if (realtimeOverride) return; - - tpmPacketCount++; //increment the packet count - if (tpmPacketCount == 1) tpmPayloadFrameSize = (udpIn[2] << 8) + udpIn[3]; //save frame size for the whole payload if this is the first packet - byte packetNum = udpIn[4]; //starts with 1! - byte numPackets = udpIn[5]; - - unsigned id = (tpmPayloadFrameSize/3)*(packetNum-1); //start LED - unsigned totalLen = strip.getLengthTotal(); - // Clamp to prevent buffer overread: loop accesses up to udpIn[tpmPayloadFrameSize + 5] - size_t currentPayloadFrameSize = (packetSize >= 5) ? min(tpmPayloadFrameSize, uint16_t(packetSize - 5)) : 0; - for (size_t i = 6; i < currentPayloadFrameSize + 4U && id < totalLen; i += 3, id++) { - setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); - } - if (tpmPacketCount == numPackets) { //reset packet count and show if all packets were received - tpmPacketCount = 0; - if (useMainSegmentOnly) strip.trigger(); - else strip.show(); - } - return; - } - - //UDP realtime: 1 warls 2 drgb 3 drgbw 4 dnrgb 5 dnrgbw - if (udpIn[0] > 0 && udpIn[0] < 6) { - realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP(); - DEBUG_PRINTLN(realtimeIP); - if (packetSize < 2) return; - - if (udpIn[1] == 0) { - realtimeTimeout = 0; // cancel realtime mode immediately - return; - } else { - realtimeLock(udpIn[1]*1000 +1, REALTIME_MODE_UDP); - } - if (realtimeOverride) return; - - unsigned totalLen = strip.getLengthTotal(); - if (udpIn[0] == 1 && packetSize > 5) { //warls - for (size_t i = 2; i < packetSize -3; i += 4) { - setRealtimePixel(udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3], 0); - } - } else if (udpIn[0] == 2 && packetSize > 4) { //drgb - for (size_t i = 2, id = 0; i < packetSize -2 && id < totalLen; i += 3, id++) - { - setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); - } - } else if (udpIn[0] == 3 && packetSize > 6) { //drgbw - for (size_t i = 2, id = 0; i < packetSize -3 && id < totalLen; i += 4, id++) { - setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]); - } - } else if (udpIn[0] == 4 && packetSize > 7) { //dnrgb - unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); - for (size_t i = 4; i < packetSize -2 && id < totalLen; i += 3, id++) { - setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); - } - } else if (udpIn[0] == 5 && packetSize > 8) { //dnrgbw - unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); - for (size_t i = 4; i < packetSize -2 && id < totalLen; i += 4, id++) { - setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]); - } - } - if (useMainSegmentOnly) strip.trigger(); - else strip.show(); - return; - } + if (handleDirectRealtimePacket(udpIn, packetSize, isSupp)) return; } // API over UDP @@ -661,327 +89,3 @@ void handleNotifications() UsermodManager::onUdpPacket(udpIn, packetSize); } - - -void setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w) -{ - unsigned pix = i + arlsOffset; - strip.setRealtimePixelColor(pix, RGBW32(r,g,b,w)); -} - -/*********************************************************************************************\ - Refresh aging for remote units, drop if too old... -\*********************************************************************************************/ -void refreshNodeList() -{ - for (NodesMap::iterator it = Nodes.begin(); it != Nodes.end();) { - bool mustRemove = true; - - if (it->second.ip[0] != 0) { - if (it->second.age < 10) { - it->second.age++; - mustRemove = false; - ++it; - } - } - - if (mustRemove) { - it = Nodes.erase(it); - } - } -} - -/*********************************************************************************************\ - Broadcast system info to other nodes. (to update node lists) -\*********************************************************************************************/ -void sendSysInfoUDP() -{ - if (!udp2Connected) return; - - IPAddress ip = WLEDNetwork.localIP(); - if (!ip || ip == IPAddress(255,255,255,255)) ip = IPAddress(4,3,2,1); - - // TODO: make a nice struct of it and clean up - // 0: 1 byte 'binary token 255' - // 1: 1 byte id '1' - // 2: 4 byte ip - // 6: 32 char name - // 38: 1 byte node type id - // 39: 1 byte node id - // 40: 4 byte version ID - // 44 bytes total - - // send my info to the world... - uint8_t data[44] = {0}; - data[0] = 255; - data[1] = 1; - - for (size_t x = 0; x < 4; x++) { - data[x + 2] = ip[x]; - } - memcpy((byte *)data + 6, serverDescription, 32); - data[38] = uint8_t(WLED_BOARD); // see wled_boards.h - if (bri) data[38] |= 0x80U; // add on/off state - data[39] = ip[3]; // unit ID == last IP number - - uint32_t build = VERSION; - for (size_t i=0; i>(8*i)) & 0xFF; - - IPAddress broadcastIP(255, 255, 255, 255); - notifier2Udp.beginPacket(broadcastIP, udpPort2); - notifier2Udp.write(data, sizeof(data)); - notifier2Udp.endPacket(); -} - - -/*********************************************************************************************\ - * Art-Net, DDP, E131 output - work in progress -\*********************************************************************************************/ - -// -// Send real time UDP updates to the specified client -// -// type - protocol type (0=DDP, 1=E1.31, 2=ArtNet) -// client - the IP address to send to -// length - the number of pixels -// buffer - a buffer of at least length*4 bytes long -// isRGBW - true if the buffer contains 4 components per pixel - -static size_t sequenceNumber = 0; // this needs to be shared across all outputs -static const size_t ART_NET_HEADER_SIZE = 12; -static const byte ART_NET_HEADER[] PROGMEM = {0x41,0x72,0x74,0x2d,0x4e,0x65,0x74,0x00,0x00,0x50,0x00,0x0e}; - -uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t *buffer, uint8_t bri, bool isRGBW) { - if (!(apActive || interfacesInited) || !client[0] || !length) return 1; // network not initialised or dummy/unset IP address 031522 ajn added check for ap - - WiFiUDP ddpUdp; - - switch (type) { - case 0: // DDP - { - // calculate the number of UDP packets we need to send - size_t channelCount = length * (isRGBW? 4:3); // 1 channel for every R,G,B value - size_t packetCount = ((channelCount-1) / DDP_CHANNELS_PER_PACKET) +1; - - // there are 3 channels per RGB pixel - uint32_t channel = 0; // TODO: allow specifying the start channel - // the current position in the buffer - size_t bufferOffset = 0; - - for (size_t currentPacket = 0; currentPacket < packetCount; currentPacket++) { - if (sequenceNumber > 15) sequenceNumber = 0; - - if (!ddpUdp.beginPacket(client, DDP_DEFAULT_PORT)) { // port defined in ESPAsyncE131.h - //DEBUG_PRINTLN(F("WiFiUDP.beginPacket returned an error")); - return 1; // problem - } - - // the amount of data is AFTER the header in the current packet - size_t packetSize = DDP_CHANNELS_PER_PACKET; - - uint8_t flags = DDP_FLAGS_VER1; - if (currentPacket == (packetCount - 1U)) { - // last packet, set the push flag - // TODO: determine if we want to send an empty push packet to each destination after sending the pixel data - flags = DDP_FLAGS_VER1 | DDP_FLAGS_PUSH; - if (channelCount % DDP_CHANNELS_PER_PACKET) { - packetSize = channelCount % DDP_CHANNELS_PER_PACKET; - } - } - - // write the header - /*0*/ddpUdp.write(flags); - // TODO: sequence number should be 1-15 as 0 means "unused", it has no bad consequences other than out of sequence packet may be accepted - /*1*/ddpUdp.write(sequenceNumber++ & 0x0F); // sequence may be unnecessary unless we are sending twice (as requested in Sync settings) - /*2*/ddpUdp.write(isRGBW ? DDP_TYPE_RGBW32 : DDP_TYPE_RGB24); - /*3*/ddpUdp.write(DDP_ID_DISPLAY); - // data offset in bytes, 32-bit number, MSB first - /*4*/ddpUdp.write(0xFF & (channel >> 24)); - /*5*/ddpUdp.write(0xFF & (channel >> 16)); - /*6*/ddpUdp.write(0xFF & (channel >> 8)); - /*7*/ddpUdp.write(0xFF & (channel )); - // data length in bytes, 16-bit number, MSB first - /*8*/ddpUdp.write(0xFF & (packetSize >> 8)); - /*9*/ddpUdp.write(0xFF & (packetSize )); - - // write the colors, the write write(const uint8_t *buffer, size_t size) - // function is just a loop internally too - for (size_t i = 0; i < packetSize; i += (isRGBW?4:3)) { - ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // R - ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // G - ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // B - if (isRGBW) ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // W - } - - if (!ddpUdp.endPacket()) { - //DEBUG_PRINTLN(F("WiFiUDP.endPacket returned an error")); - return 1; // problem - } - - channel += packetSize; - } - } break; - - case 1: //E1.31 - { - } break; - - case 2: //ArtNet - { - // calculate the number of UDP packets we need to send - const size_t channelCount = length * (isRGBW?4:3); // 1 channel for every R,G,B,(W?) value - const size_t ARTNET_CHANNELS_PER_PACKET = isRGBW?512:510; // 512/4=128 RGBW LEDs, 510/3=170 RGB LEDs - const size_t packetCount = ((channelCount-1)/ARTNET_CHANNELS_PER_PACKET)+1; - - uint32_t channel = 0; - size_t bufferOffset = 0; - - sequenceNumber++; - - for (size_t currentPacket = 0; currentPacket < packetCount; currentPacket++) { - - if (sequenceNumber > 255) sequenceNumber = 0; - - if (!ddpUdp.beginPacket(client, ARTNET_DEFAULT_PORT)) { - DEBUG_PRINTLN(F("Art-Net WiFiUDP.beginPacket returned an error")); - return 1; // borked - } - - size_t packetSize = ARTNET_CHANNELS_PER_PACKET; - - if (currentPacket == (packetCount - 1U)) { - // last packet - if (channelCount % ARTNET_CHANNELS_PER_PACKET) { - packetSize = channelCount % ARTNET_CHANNELS_PER_PACKET; - } - } - - byte header_buffer[ART_NET_HEADER_SIZE]; - memcpy_P(header_buffer, ART_NET_HEADER, ART_NET_HEADER_SIZE); - ddpUdp.write(header_buffer, ART_NET_HEADER_SIZE); // This doesn't change. Hard coded ID, OpCode, and protocol version. - ddpUdp.write(sequenceNumber & 0xFF); // sequence number. 1..255 - ddpUdp.write(0x00); // physical - more an FYI, not really used for anything. 0..3 - ddpUdp.write((currentPacket) & 0xFF); // Universe LSB. 1 full packet == 1 full universe, so just use current packet number. - ddpUdp.write(0x00); // Universe MSB, unused. - ddpUdp.write(0xFF & (packetSize >> 8)); // 16-bit length of channel data, MSB - ddpUdp.write(0xFF & (packetSize )); // 16-bit length of channel data, LSB - - for (size_t i = 0; i < packetSize; i += (isRGBW?4:3)) { - ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // R - ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // G - ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // B - if (isRGBW) ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // W - } - - if (!ddpUdp.endPacket()) { - DEBUG_PRINTLN(F("Art-Net WiFiUDP.endPacket returned an error")); - return 1; // borked - } - channel += packetSize; - } - } break; - } - return 0; -} - -#ifndef WLED_DISABLE_ESPNOW -// ESP-NOW message sent callback function -void espNowSentCB(uint8_t* address, uint8_t status) { - DEBUG_PRINTF_P(PSTR("Message sent to " MACSTR ", status: %d\n"), MAC2STR(address), status); -} - -// ESP-NOW message receive callback function -void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rssi, bool broadcast) { - sprintf_P(last_signal_src, PSTR("%02x%02x%02x%02x%02x%02x"), address[0], address[1], address[2], address[3], address[4], address[5]); - - #ifdef WLED_DEBUG - DEBUG_PRINT(F("ESP-NOW: ")); DEBUG_PRINT(last_signal_src); DEBUG_PRINT(F(" -> ")); DEBUG_PRINTLN(len); - for (int i=0; i(data); - if (len < 3 || !broadcast || buffer->magic != 'W' || !useESPNowSync || WLED_CONNECTED) { - DEBUG_PRINTLN(F("ESP-NOW unexpected packet, not syncing or connected to WiFi.")); - return; - } - - static uint8_t *udpIn = nullptr; - static uint8_t packetsReceived = 0; - static uint8_t segsReceived = 0; - static unsigned long lastProcessed = 0; - - if (buffer->packet == 0) { - packetsReceived = 0; // it will increment later (this is to make sure we start counting packets correctly) - if (udpIn == nullptr) { - udpIn = (uint8_t *)malloc(WLEDPACKETSIZE); // we cannot use stack as we are in callback - if (!udpIn) return; // memory alocation failed - DEBUG_PRINTLN(F("ESP-NOW inited UDP buffer.")); - } - memcpy(udpIn, buffer->data, len-3); // global data (41 bytes + up to 5 segments) - segsReceived = (len - 3 - 41) / UDP_SEG_SIZE; - } else if (buffer->packet == packetsReceived && udpIn && ((len - 3) / UDP_SEG_SIZE) * UDP_SEG_SIZE == (len-3)) { - // we received a packet full of segments - if (segsReceived >= MAX_NUM_SEGMENTS) { - // we are already past max segments, just ignore - DEBUG_PRINTLN(F("ESP-NOW received segments past maximum.")); - len = 3; - } else if ((segsReceived + ((len - 3) / UDP_SEG_SIZE)) >= MAX_NUM_SEGMENTS) { - len = ((MAX_NUM_SEGMENTS - segsReceived) * UDP_SEG_SIZE) + 3; // we have reached max number of segments - } - if (len > 3) { - memcpy(udpIn + 41 + (segsReceived * UDP_SEG_SIZE), buffer->data, len-3); - segsReceived += (len - 3) / UDP_SEG_SIZE; - } - } else { - // any out of order packet or incorrectly sized packet or if we have no UDP buffer will abort - DEBUG_PRINTF_P(PSTR("ESP-NOW incorrect packet: %d (%d) [%d]\n"), (int)buffer->packet, (int)len-3, (int)UDP_SEG_SIZE); - if (udpIn) free(udpIn); - udpIn = nullptr; - packetsReceived = 0; - segsReceived = 0; - return; - } - if (!udpIn) return; - - packetsReceived++; - DEBUG_PRINTF_P(PSTR("ESP-NOW packet received: %d (%d/%d) s:[%d/%d]\n"), (int)buffer->packet, (int)packetsReceived, (int)buffer->noOfPackets, (int)segsReceived, MAX_NUM_SEGMENTS); - if (packetsReceived >= buffer->noOfPackets) { - // last packet received - if (millis() - lastProcessed > 250) { - DEBUG_PRINTLN(F("ESP-NOW processing complete message.")); - parseNotifyPacket(udpIn); - lastProcessed = millis(); - } else { - DEBUG_PRINTLN(F("ESP-NOW ignoring complete message.")); - } - free(udpIn); - udpIn = nullptr; - packetsReceived = 0; - segsReceived = 0; - } -} -#endif diff --git a/wled00/wled.h b/wled00/wled.h index 9bafb49196..8ae04f2384 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -653,9 +653,8 @@ WLED_GLOBAL byte touchThreshold _INIT(TOUCH_THRESH // notifications WLED_GLOBAL bool sendNotifications _INIT(false); // master notification switch WLED_GLOBAL bool sendNotificationsRT _INIT(false); // master notification switch (runtime) -WLED_GLOBAL unsigned long notificationSentTime _INIT(0); -WLED_GLOBAL byte notificationSentCallMode _INIT(CALL_MODE_INIT); -WLED_GLOBAL uint8_t notificationCount _INIT(0); +// notify() send-retry bookkeeping lives in sync_notifier.cpp (NotifierSendState) - +// nothing outside that file needs it; call notifyRetryIfNeeded() instead. WLED_GLOBAL uint8_t syncGroups _INIT(0x01); // sync send groups this instance syncs to (bit mapped) WLED_GLOBAL uint8_t receiveGroups _INIT(0x01); // sync receive groups this instance belongs to (bit mapped) WLED_GLOBAL bool receiveNotificationBrightness _INIT(true); // apply brightness from incoming notifications @@ -726,8 +725,6 @@ WLED_GLOBAL byte realtimeMode _INIT(REALTIME_MODE_INACTIVE); WLED_GLOBAL byte realtimeOverride _INIT(REALTIME_OVERRIDE_NONE); WLED_GLOBAL IPAddress realtimeIP _INIT_N(((0, 0, 0, 0))); WLED_GLOBAL unsigned long realtimeTimeout _INIT(0); -WLED_GLOBAL uint8_t tpmPacketCount _INIT(0); -WLED_GLOBAL uint16_t tpmPayloadFrameSize _INIT(0); WLED_GLOBAL bool useMainSegmentOnly _INIT(false); WLED_GLOBAL bool realtimeRespectLedMaps _INIT(true); // Respect LED maps when receiving realtime data From 01cbf9ca6af6244eea7f8c136213270f8ceb3abb Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 8 Aug 2026 09:24:23 +0100 Subject: [PATCH 2/2] Move sync files into wled00/sync/, keep udp.cpp at top level udp.cpp stays where contributors expect it (still just the handleNotifications() dispatcher); the six files it dispatches to move into wled00/sync/ to signal they're a cohesive unit distinct from the rest of wled00/'s flat file layout. Only change needed beyond the file moves: udp.cpp's #include "sync.h" becomes #include "sync/sync.h" since udp.cpp no longer shares a directory with it. The three moved files that also include sync.h (sync_notifier.cpp, realtime_udp.cpp, espnow_sync.cpp) need no change, since they now share a directory with it. Confirmed via the actual compiler invocation that wled00/ is already passed as an explicit -I, so #include "wled.h" continues to resolve correctly from the subfolder with no build_flags/platformio.ini changes required. Verified: nodemcuv2 builds and links cleanly (933555 bytes flash, +16 bytes vs. before the move - just the changed include path). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01UtCyBD91vAYWvBzaMyQSHd --- wled00/{ => sync}/espnow_sync.cpp | 0 wled00/{ => sync}/realtime.cpp | 0 wled00/{ => sync}/realtime_broadcast.cpp | 0 wled00/{ => sync}/realtime_udp.cpp | 0 wled00/{ => sync}/sync.h | 0 wled00/{ => sync}/sync_nodes.cpp | 0 wled00/{ => sync}/sync_notifier.cpp | 0 wled00/udp.cpp | 2 +- 8 files changed, 1 insertion(+), 1 deletion(-) rename wled00/{ => sync}/espnow_sync.cpp (100%) rename wled00/{ => sync}/realtime.cpp (100%) rename wled00/{ => sync}/realtime_broadcast.cpp (100%) rename wled00/{ => sync}/realtime_udp.cpp (100%) rename wled00/{ => sync}/sync.h (100%) rename wled00/{ => sync}/sync_nodes.cpp (100%) rename wled00/{ => sync}/sync_notifier.cpp (100%) diff --git a/wled00/espnow_sync.cpp b/wled00/sync/espnow_sync.cpp similarity index 100% rename from wled00/espnow_sync.cpp rename to wled00/sync/espnow_sync.cpp diff --git a/wled00/realtime.cpp b/wled00/sync/realtime.cpp similarity index 100% rename from wled00/realtime.cpp rename to wled00/sync/realtime.cpp diff --git a/wled00/realtime_broadcast.cpp b/wled00/sync/realtime_broadcast.cpp similarity index 100% rename from wled00/realtime_broadcast.cpp rename to wled00/sync/realtime_broadcast.cpp diff --git a/wled00/realtime_udp.cpp b/wled00/sync/realtime_udp.cpp similarity index 100% rename from wled00/realtime_udp.cpp rename to wled00/sync/realtime_udp.cpp diff --git a/wled00/sync.h b/wled00/sync/sync.h similarity index 100% rename from wled00/sync.h rename to wled00/sync/sync.h diff --git a/wled00/sync_nodes.cpp b/wled00/sync/sync_nodes.cpp similarity index 100% rename from wled00/sync_nodes.cpp rename to wled00/sync/sync_nodes.cpp diff --git a/wled00/sync_notifier.cpp b/wled00/sync/sync_notifier.cpp similarity index 100% rename from wled00/sync_notifier.cpp rename to wled00/sync/sync_notifier.cpp diff --git a/wled00/udp.cpp b/wled00/udp.cpp index b733db585b..f40714db40 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -1,5 +1,5 @@ #include "wled.h" -#include "sync.h" +#include "sync/sync.h" /* * UDP sync dispatcher.