From f2797c00e29a12679599cef1e92ca1a9e3a1a858 Mon Sep 17 00:00:00 2001 From: spectrenoir06 Date: Sat, 6 Dec 2025 20:51:36 +0100 Subject: [PATCH 1/8] Add JSON data handling over ESP-NOW in espNowReceiveCB --- wled00/udp.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 26a80ac349..5f382f0a40 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -952,6 +952,19 @@ void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rs return; } + // handle JSON data over ESP-NOW + if (data[0] == '{') { + if (requestJSONBufferLock(18)) { + DeserializationError error = deserializeJson(*pDoc, data, len); + JsonObject root = pDoc->as(); + if (!error && !root.isNull()) { + deserializeState(root); + } + releaseJSONBufferLock(); + } + return; + } + partial_packet_t *buffer = reinterpret_cast(data); if (len < 3 || !broadcast || buffer->magic != 'W' || !useESPNowSync || WLED_CONNECTED) { DEBUG_PRINTLN(F("ESP-NOW unexpected packet, not syncing or connected to WiFi.")); From 124b9f984e2b32b5c42afcb324538138484e93b7 Mon Sep 17 00:00:00 2001 From: spectrenoir06 Date: Sun, 25 Jan 2026 18:32:25 +0100 Subject: [PATCH 2/8] Implement ESP-NOW JSON message fragmentation handling and reassembly in UDP ( need to me moved in usermod ) --- wled00/udp.cpp | 131 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 119 insertions(+), 12 deletions(-) diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 5f382f0a40..ed56055164 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -914,12 +914,129 @@ uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const return 0; } +// ESP-NOW limits +#define ESPNOW_MAX_PAYLOAD 250 +#define FRAGMENT_HEADER_SIZE 4 +#define FRAGMENT_DATA_SIZE (ESPNOW_MAX_PAYLOAD - FRAGMENT_HEADER_SIZE) + +// Fragment header structure +typedef struct { + uint8_t message_id; // Unique message ID for reassembly + uint8_t fragment_index; // Current fragment number (0-based) + uint8_t total_fragments; // Total number of fragments in message +} __attribute__((packed)) fragment_header_t; + #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); } +void handleJsonEspNow(uint8_t* address, uint8_t* data, uint8_t len) { + static uint8_t last_msg_id = 0; + static uint8_t fragments_received = 0; + static uint8_t *reassembly_buffer = nullptr; + static size_t reassembly_size = 0; + + // handle fragmented JSON messages + if (len >= FRAGMENT_HEADER_SIZE) { + static uint8_t last_processed_msg_id = 0; + + uint8_t message_id = data[0]; + uint8_t fragment_index = data[1]; + uint8_t total_fragments = data[2]; + + DEBUG_PRINTF_P(PSTR("ESP-NOW fragment %d/%d of message %d (%d bytes)\n"), fragment_index + 1, total_fragments, message_id, len - FRAGMENT_HEADER_SIZE); + + // Check if this message was already processed (deduplication for multi-channel reception) + if (message_id == last_processed_msg_id) { + DEBUG_PRINTF_P(PSTR("ESP-NOW message %d already processed, skipping\n"), message_id); + // If we're currently reassembling this message, clean up + if (message_id == last_msg_id && reassembly_buffer) { + free(reassembly_buffer); + reassembly_buffer = nullptr; + fragments_received = 0; + reassembly_size = 0; + } + return; + } + + // Check if this is a new message + if (message_id != last_msg_id) { + // Clean up old reassembly buffer if exists + if (reassembly_buffer) { + free(reassembly_buffer); + reassembly_buffer = nullptr; + } + last_msg_id = message_id; + fragments_received = 0; + reassembly_size = 0; + } + + // Validate fragment index is sequential + if (fragment_index != fragments_received) { + DEBUG_PRINTF_P(PSTR("ESP-NOW fragment out of order: expected %d, got %d\n"), fragments_received, fragment_index); + if (reassembly_buffer) { + free(reassembly_buffer); + reassembly_buffer = nullptr; + } + fragments_received = 0; + reassembly_size = 0; + return; + } + + // Allocate or reallocate buffer + size_t fragment_data_size = len - FRAGMENT_HEADER_SIZE; + size_t new_size = reassembly_size + fragment_data_size; + + uint8_t *new_buffer = (uint8_t *)realloc(reassembly_buffer, new_size + 1); // +1 for null terminator + if (!new_buffer) { + DEBUG_PRINTLN(F("ESP-NOW fragment reassembly: memory allocation failed")); + if (reassembly_buffer) free(reassembly_buffer); + reassembly_buffer = nullptr; + fragments_received = 0; + reassembly_size = 0; + return; + } + + reassembly_buffer = new_buffer; + + // Copy fragment data + memcpy(reassembly_buffer + reassembly_size, data + FRAGMENT_HEADER_SIZE, fragment_data_size); + reassembly_size = new_size; + fragments_received++; + + // Check if we have all fragments + if (fragments_received >= total_fragments) { + reassembly_buffer[reassembly_size] = '\0'; // Null terminate + DEBUG_PRINTF_P(PSTR("ESP-NOW complete message reassembled (%d bytes)\n"), reassembly_size); + + // Mark this message as processed for deduplication + last_processed_msg_id = message_id; + + // Process the complete JSON message + if (requestJSONBufferLock(18)) { + DeserializationError error = deserializeJson(*pDoc, reassembly_buffer, reassembly_size); + JsonObject root = pDoc->as(); + if (!error && !root.isNull()) { + deserializeState(root); + } else { + DEBUG_PRINTF_P(PSTR("ESP-NOW JSON deserialization error: %s\n"), error.c_str()); + } + releaseJSONBufferLock(); + } + + // Clean up + free(reassembly_buffer); + reassembly_buffer = nullptr; + fragments_received = 0; + reassembly_size = 0; + } + return; + } +} + + // 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]); @@ -952,18 +1069,8 @@ void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rs return; } - // handle JSON data over ESP-NOW - if (data[0] == '{') { - if (requestJSONBufferLock(18)) { - DeserializationError error = deserializeJson(*pDoc, data, len); - JsonObject root = pDoc->as(); - if (!error && !root.isNull()) { - deserializeState(root); - } - releaseJSONBufferLock(); - } - return; - } + handleJsonEspNow(address, data, len); + partial_packet_t *buffer = reinterpret_cast(data); if (len < 3 || !broadcast || buffer->magic != 'W' || !useESPNowSync || WLED_CONNECTED) { From e7e399933ad8c12996b4da9b1107c65faddb58d9 Mon Sep 17 00:00:00 2001 From: spectrenoir06 Date: Sun, 25 Jan 2026 19:27:48 +0100 Subject: [PATCH 3/8] Add ESP-NOW JSON Handler usermod for fragmented message processing --- .../espnow_json_handler.cpp | 158 ++++++++++++++++++ usermods/espnow_json_handler/library.json | 5 + usermods/espnow_json_handler/readme.md | 42 +++++ wled00/const.h | 1 + wled00/udp.cpp | 118 ------------- 5 files changed, 206 insertions(+), 118 deletions(-) create mode 100644 usermods/espnow_json_handler/espnow_json_handler.cpp create mode 100644 usermods/espnow_json_handler/library.json create mode 100644 usermods/espnow_json_handler/readme.md diff --git a/usermods/espnow_json_handler/espnow_json_handler.cpp b/usermods/espnow_json_handler/espnow_json_handler.cpp new file mode 100644 index 0000000000..16f3091b4e --- /dev/null +++ b/usermods/espnow_json_handler/espnow_json_handler.cpp @@ -0,0 +1,158 @@ +#include "wled.h" + +#ifndef WLED_DISABLE_ESPNOW + +/* + * ESP-NOW JSON Handler Usermod + * + * This usermod handles fragmented JSON messages received via ESP-NOW. + * It reassembles fragments and deserializes the complete JSON payload + * to control WLED state. + * + * Fragment header structure (4 bytes): + * - Byte 0: message_id (unique identifier for reassembly) + * - Byte 1: fragment_index (0-based fragment number) + * - Byte 2: total_fragments (total number of fragments in message) + * - Byte 3+: JSON data payload + */ + +#define ESPNOW_FRAGMENT_HEADER_SIZE 3 + +class EspNowJsonHandler : public Usermod { + +private: + // Fragment reassembly state + uint8_t lastMsgId = 0; + uint8_t lastProcessedMsgId = 0; + uint8_t fragmentsReceived = 0; + uint8_t* reassemblyBuffer = nullptr; + size_t reassemblySize = 0; + + // Cleanup reassembly state + void cleanupReassembly() { + if (reassemblyBuffer) { + free(reassemblyBuffer); + reassemblyBuffer = nullptr; + } + fragmentsReceived = 0; + reassemblySize = 0; + } + +public: + void setup() override { + // Nothing to initialize + } + + void loop() override { + // Nothing to do in loop + } + + /** + * Handle incoming ESP-NOW messages + * Returns true if the message was handled (prevents default processing) + */ + bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) override { + // Need at least header size to process + if (len < ESPNOW_FRAGMENT_HEADER_SIZE) { + return false; + } + + // Check if this looks like a fragmented JSON message + // First byte should be a reasonable message ID (not a WiZ Mote signature or 'W' sync packet) + if (payload[0] == 0x91 || payload[0] == 0x81 || payload[0] == 0x80 || payload[0] == 'W') { + return false; // Let default handlers process these + } + + uint8_t messageId = payload[0]; + uint8_t fragmentIndex = payload[1]; + uint8_t totalFragments = payload[2]; + + // Validate fragment header - sanity checks + if (totalFragments == 0 || fragmentIndex >= totalFragments) { + return false; // Invalid fragment header + } + + DEBUG_PRINTF_P(PSTR("ESP-NOW JSON fragment %d/%d of message %d (%d bytes)\n"), + fragmentIndex + 1, totalFragments, messageId, len - ESPNOW_FRAGMENT_HEADER_SIZE); + + // Check if this message was already processed (deduplication for multi-channel reception) + if (messageId == lastProcessedMsgId) { + DEBUG_PRINTF_P(PSTR("ESP-NOW message %d already processed, skipping\n"), messageId); + // If we're currently reassembling this message, clean up + if (messageId == lastMsgId && reassemblyBuffer) { + cleanupReassembly(); + } + return true; // Message was handled (by ignoring duplicate) + } + + // Check if this is a new message + if (messageId != lastMsgId) { + // Clean up old reassembly buffer if exists + cleanupReassembly(); + lastMsgId = messageId; + } + + // Validate fragment index is sequential + if (fragmentIndex != fragmentsReceived) { + DEBUG_PRINTF_P(PSTR("ESP-NOW fragment out of order: expected %d, got %d\n"), + fragmentsReceived, fragmentIndex); + cleanupReassembly(); + return true; // Handled by aborting + } + + // Allocate or reallocate buffer + size_t fragmentDataSize = len - ESPNOW_FRAGMENT_HEADER_SIZE; + size_t newSize = reassemblySize + fragmentDataSize; + + uint8_t* newBuffer = (uint8_t*)realloc(reassemblyBuffer, newSize + 1); // +1 for null terminator + if (!newBuffer) { + DEBUG_PRINTLN(F("ESP-NOW fragment reassembly: memory allocation failed")); + cleanupReassembly(); + return true; // Handled by failing gracefully + } + + reassemblyBuffer = newBuffer; + + // Copy fragment data + memcpy(reassemblyBuffer + reassemblySize, payload + ESPNOW_FRAGMENT_HEADER_SIZE, fragmentDataSize); + reassemblySize = newSize; + fragmentsReceived++; + + // Check if we have all fragments + if (fragmentsReceived >= totalFragments) { + reassemblyBuffer[reassemblySize] = '\0'; // Null terminate + DEBUG_PRINTF_P(PSTR("ESP-NOW complete message reassembled (%d bytes): %s\n"), reassemblySize, (char*)reassemblyBuffer); + + // Mark this message as processed for deduplication + lastProcessedMsgId = messageId; + + // Process the complete JSON message + if (requestJSONBufferLock(18)) { + DeserializationError error = deserializeJson(*pDoc, reassemblyBuffer, reassemblySize); + JsonObject root = pDoc->as(); + if (!error && !root.isNull()) { + deserializeState(root); + } + else { + DEBUG_PRINTF_P(PSTR("ESP-NOW JSON deserialization error: %s\n"), error.c_str()); + } + releaseJSONBufferLock(); + } + + // Clean up + cleanupReassembly(); + } + + return true; // Message was handled + } + + uint16_t getId() override { + return USERMOD_ID_ESPNOW_JSON; + } +}; + +// Allocate static instance and register with WLED +static EspNowJsonHandler espNowJsonHandler; +REGISTER_USERMOD(espNowJsonHandler); + +#endif // WLED_DISABLE_ESPNOW diff --git a/usermods/espnow_json_handler/library.json b/usermods/espnow_json_handler/library.json new file mode 100644 index 0000000000..97c25f054a --- /dev/null +++ b/usermods/espnow_json_handler/library.json @@ -0,0 +1,5 @@ +{ + "name": "espnow_json_handler", + "build": { "libArchive": false }, + "dependencies": {} +} diff --git a/usermods/espnow_json_handler/readme.md b/usermods/espnow_json_handler/readme.md new file mode 100644 index 0000000000..7ca61ae36c --- /dev/null +++ b/usermods/espnow_json_handler/readme.md @@ -0,0 +1,42 @@ +# ESP-NOW JSON Handler Usermod + +This usermod handles fragmented JSON messages received via ESP-NOW, allowing you to control WLED state from ESP-NOW enabled devices. + +## Features + +- Receives fragmented JSON messages over ESP-NOW +- Reassembles fragments into complete JSON payloads +- Deduplication to prevent processing duplicate messages when broadcasting on multiple channels + +## Fragment Protocol + +Messages are fragmented with a 3-byte header: + +| Byte | Description | +|------|-------------| +| 0 | Message ID (unique identifier for reassembly) | +| 1 | Fragment Index | +| 2 | Total Fragments | +| 3+ | JSON data payload | + +## Usage + +1. Enable ESP-NOW in WLED settings +2. Add the sender's MAC address to the linked remotes list +3. Send fragmented JSON payloads following the protocol above + +## Compilation + +Add the following to your `platformio_override.ini`: + +```ini +[env:yourenv] +extends = env:esp32dev +custom_usermods = espnow_json_handler +``` + +## Notes + +- ESP-NOW must be enabled (`WLED_DISABLE_ESPNOW` must NOT be defined) +- The sender MAC address must be in the linked remotes list +- Maximum ESP-NOW payload per fragment is 250 bytes (247 bytes of JSON data) for V1 versions and 1470 bytes (1467 bytes of JSON data) for V2 versions diff --git a/wled00/const.h b/wled00/const.h index ac48838435..adb1b2c0cd 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -207,6 +207,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define USERMOD_ID_RF433 56 //Usermod "usermod_v2_RF433.h" #define USERMOD_ID_BRIGHTNESS_FOLLOW_SUN 57 //Usermod "usermod_v2_brightness_follow_sun.h" #define USERMOD_ID_USER_FX 58 //Usermod "user_fx" +#define USERMOD_ID_ESPNOW_JSON 59 //Usermod "espnow_json_handler.cpp" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot diff --git a/wled00/udp.cpp b/wled00/udp.cpp index ed56055164..27acb65471 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -914,129 +914,12 @@ uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const return 0; } -// ESP-NOW limits -#define ESPNOW_MAX_PAYLOAD 250 -#define FRAGMENT_HEADER_SIZE 4 -#define FRAGMENT_DATA_SIZE (ESPNOW_MAX_PAYLOAD - FRAGMENT_HEADER_SIZE) - -// Fragment header structure -typedef struct { - uint8_t message_id; // Unique message ID for reassembly - uint8_t fragment_index; // Current fragment number (0-based) - uint8_t total_fragments; // Total number of fragments in message -} __attribute__((packed)) fragment_header_t; - #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); } -void handleJsonEspNow(uint8_t* address, uint8_t* data, uint8_t len) { - static uint8_t last_msg_id = 0; - static uint8_t fragments_received = 0; - static uint8_t *reassembly_buffer = nullptr; - static size_t reassembly_size = 0; - - // handle fragmented JSON messages - if (len >= FRAGMENT_HEADER_SIZE) { - static uint8_t last_processed_msg_id = 0; - - uint8_t message_id = data[0]; - uint8_t fragment_index = data[1]; - uint8_t total_fragments = data[2]; - - DEBUG_PRINTF_P(PSTR("ESP-NOW fragment %d/%d of message %d (%d bytes)\n"), fragment_index + 1, total_fragments, message_id, len - FRAGMENT_HEADER_SIZE); - - // Check if this message was already processed (deduplication for multi-channel reception) - if (message_id == last_processed_msg_id) { - DEBUG_PRINTF_P(PSTR("ESP-NOW message %d already processed, skipping\n"), message_id); - // If we're currently reassembling this message, clean up - if (message_id == last_msg_id && reassembly_buffer) { - free(reassembly_buffer); - reassembly_buffer = nullptr; - fragments_received = 0; - reassembly_size = 0; - } - return; - } - - // Check if this is a new message - if (message_id != last_msg_id) { - // Clean up old reassembly buffer if exists - if (reassembly_buffer) { - free(reassembly_buffer); - reassembly_buffer = nullptr; - } - last_msg_id = message_id; - fragments_received = 0; - reassembly_size = 0; - } - - // Validate fragment index is sequential - if (fragment_index != fragments_received) { - DEBUG_PRINTF_P(PSTR("ESP-NOW fragment out of order: expected %d, got %d\n"), fragments_received, fragment_index); - if (reassembly_buffer) { - free(reassembly_buffer); - reassembly_buffer = nullptr; - } - fragments_received = 0; - reassembly_size = 0; - return; - } - - // Allocate or reallocate buffer - size_t fragment_data_size = len - FRAGMENT_HEADER_SIZE; - size_t new_size = reassembly_size + fragment_data_size; - - uint8_t *new_buffer = (uint8_t *)realloc(reassembly_buffer, new_size + 1); // +1 for null terminator - if (!new_buffer) { - DEBUG_PRINTLN(F("ESP-NOW fragment reassembly: memory allocation failed")); - if (reassembly_buffer) free(reassembly_buffer); - reassembly_buffer = nullptr; - fragments_received = 0; - reassembly_size = 0; - return; - } - - reassembly_buffer = new_buffer; - - // Copy fragment data - memcpy(reassembly_buffer + reassembly_size, data + FRAGMENT_HEADER_SIZE, fragment_data_size); - reassembly_size = new_size; - fragments_received++; - - // Check if we have all fragments - if (fragments_received >= total_fragments) { - reassembly_buffer[reassembly_size] = '\0'; // Null terminate - DEBUG_PRINTF_P(PSTR("ESP-NOW complete message reassembled (%d bytes)\n"), reassembly_size); - - // Mark this message as processed for deduplication - last_processed_msg_id = message_id; - - // Process the complete JSON message - if (requestJSONBufferLock(18)) { - DeserializationError error = deserializeJson(*pDoc, reassembly_buffer, reassembly_size); - JsonObject root = pDoc->as(); - if (!error && !root.isNull()) { - deserializeState(root); - } else { - DEBUG_PRINTF_P(PSTR("ESP-NOW JSON deserialization error: %s\n"), error.c_str()); - } - releaseJSONBufferLock(); - } - - // Clean up - free(reassembly_buffer); - reassembly_buffer = nullptr; - fragments_received = 0; - reassembly_size = 0; - } - return; - } -} - - // 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]); @@ -1069,7 +952,6 @@ void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rs return; } - handleJsonEspNow(address, data, len); partial_packet_t *buffer = reinterpret_cast(data); From 3ae4c1555f7843d7d710e2b07a1956a8e2f35e12 Mon Sep 17 00:00:00 2001 From: spectrenoir06 Date: Sun, 25 Jan 2026 19:46:54 +0100 Subject: [PATCH 4/8] Fix fragment header size in ESP-NOW JSON Handler and implement reassembly timeout handling + use whitelist for remote --- .../espnow_json_handler.cpp | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/usermods/espnow_json_handler/espnow_json_handler.cpp b/usermods/espnow_json_handler/espnow_json_handler.cpp index 16f3091b4e..d717c2adc5 100644 --- a/usermods/espnow_json_handler/espnow_json_handler.cpp +++ b/usermods/espnow_json_handler/espnow_json_handler.cpp @@ -9,7 +9,7 @@ * It reassembles fragments and deserializes the complete JSON payload * to control WLED state. * - * Fragment header structure (4 bytes): + * Fragment header structure (3 bytes): * - Byte 0: message_id (unique identifier for reassembly) * - Byte 1: fragment_index (0-based fragment number) * - Byte 2: total_fragments (total number of fragments in message) @@ -17,6 +17,7 @@ */ #define ESPNOW_FRAGMENT_HEADER_SIZE 3 +#define ESPNOW_REASSEMBLY_TIMEOUT_MS 5000 // 5 second timeout for incomplete fragment reassembly class EspNowJsonHandler : public Usermod { @@ -27,6 +28,7 @@ class EspNowJsonHandler : public Usermod { uint8_t fragmentsReceived = 0; uint8_t* reassemblyBuffer = nullptr; size_t reassemblySize = 0; + unsigned long reassemblyStartTime = 0; // Timestamp when reassembly began // Cleanup reassembly state void cleanupReassembly() { @@ -36,6 +38,7 @@ class EspNowJsonHandler : public Usermod { } fragmentsReceived = 0; reassemblySize = 0; + reassemblyStartTime = 0; } public: @@ -44,7 +47,13 @@ class EspNowJsonHandler : public Usermod { } void loop() override { - // Nothing to do in loop + // Check for stale reassembly state and clean up if timed out + if (reassemblyBuffer && reassemblyStartTime > 0) { + if (millis() - reassemblyStartTime > ESPNOW_REASSEMBLY_TIMEOUT_MS) { + DEBUG_PRINTF_P(PSTR("ESP-NOW reassembly timeout for message %d, discarding %d fragments\n"), lastMsgId, fragmentsReceived); + cleanupReassembly(); + } + } } /** @@ -52,9 +61,23 @@ class EspNowJsonHandler : public Usermod { * Returns true if the message was handled (prevents default processing) */ bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) override { + + bool knownRemote = false; + for (const auto& mac : linked_remotes) { + if (strlen(mac.data()) == 12 && strcmp(last_signal_src, mac.data()) == 0) { + knownRemote = true; + break; + } + } + if (!knownRemote) { + DEBUG_PRINT(F("ESP Now Message Received from Unlinked Sender: ")); + DEBUG_PRINTLN(last_signal_src); + return false; // Not handled + } + // Need at least header size to process if (len < ESPNOW_FRAGMENT_HEADER_SIZE) { - return false; + return false; // Not handled } // Check if this looks like a fragmented JSON message @@ -90,6 +113,7 @@ class EspNowJsonHandler : public Usermod { // Clean up old reassembly buffer if exists cleanupReassembly(); lastMsgId = messageId; + reassemblyStartTime = millis(); // Start timeout timer for new message } // Validate fragment index is sequential From 737dafcc3b1b0ebb84c7a1844693388403235f9d Mon Sep 17 00:00:00 2001 From: spectrenoir06 Date: Sun, 25 Jan 2026 19:53:32 +0100 Subject: [PATCH 5/8] Enhance documentation for methods in ESP-NOW JSON Handler --- .../espnow_json_handler.cpp | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/usermods/espnow_json_handler/espnow_json_handler.cpp b/usermods/espnow_json_handler/espnow_json_handler.cpp index d717c2adc5..fd79080862 100644 --- a/usermods/espnow_json_handler/espnow_json_handler.cpp +++ b/usermods/espnow_json_handler/espnow_json_handler.cpp @@ -30,7 +30,11 @@ class EspNowJsonHandler : public Usermod { size_t reassemblySize = 0; unsigned long reassemblyStartTime = 0; // Timestamp when reassembly began - // Cleanup reassembly state + /** + * Cleanup reassembly state + * Frees the reassembly buffer and resets all fragment tracking variables. + * Called when reassembly completes, times out, or encounters an error. + */ void cleanupReassembly() { if (reassemblyBuffer) { free(reassemblyBuffer); @@ -42,10 +46,19 @@ class EspNowJsonHandler : public Usermod { } public: + /** + * Called once at startup + * No initialization required for this usermod. + */ void setup() override { // Nothing to initialize } + /** + * Called every loop iteration + * Monitors for stale fragment reassembly and cleans up timed-out messages + * to prevent memory leaks from incomplete transmissions. + */ void loop() override { // Check for stale reassembly state and clean up if timed out if (reassemblyBuffer && reassemblyStartTime > 0) { @@ -170,6 +183,10 @@ class EspNowJsonHandler : public Usermod { return true; // Message was handled } + /** + * Returns the unique identifier for this usermod + * @return USERMOD_ID_ESPNOW_JSON + */ uint16_t getId() override { return USERMOD_ID_ESPNOW_JSON; } From 222e028447c4e4d53f489ce9095f3c83092ed97e Mon Sep 17 00:00:00 2001 From: spectrenoir06 Date: Sun, 25 Jan 2026 20:01:00 +0100 Subject: [PATCH 6/8] Use sender param in onEspNowMessage and not last_signal_src --- usermods/espnow_json_handler/espnow_json_handler.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/usermods/espnow_json_handler/espnow_json_handler.cpp b/usermods/espnow_json_handler/espnow_json_handler.cpp index fd79080862..efdfd9f4d9 100644 --- a/usermods/espnow_json_handler/espnow_json_handler.cpp +++ b/usermods/espnow_json_handler/espnow_json_handler.cpp @@ -28,6 +28,7 @@ class EspNowJsonHandler : public Usermod { uint8_t fragmentsReceived = 0; uint8_t* reassemblyBuffer = nullptr; size_t reassemblySize = 0; + char _last_signal_src[13]; unsigned long reassemblyStartTime = 0; // Timestamp when reassembly began /** @@ -74,17 +75,18 @@ class EspNowJsonHandler : public Usermod { * Returns true if the message was handled (prevents default processing) */ bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) override { - + sprintf_P(_last_signal_src, PSTR("%02x%02x%02x%02x%02x%02x"), sender[0], sender[1], sender[2], sender[3], sender[4], sender[5]); + bool knownRemote = false; for (const auto& mac : linked_remotes) { - if (strlen(mac.data()) == 12 && strcmp(last_signal_src, mac.data()) == 0) { + if (strlen(mac.data()) == 12 && strcmp(_last_signal_src, mac.data()) == 0) { knownRemote = true; break; } } if (!knownRemote) { DEBUG_PRINT(F("ESP Now Message Received from Unlinked Sender: ")); - DEBUG_PRINTLN(last_signal_src); + DEBUG_PRINTLN(_last_signal_src); return false; // Not handled } From 1860e76d4e3a4761b637ff425a84fcdb7e13b53e Mon Sep 17 00:00:00 2001 From: spectrenoir06 Date: Sun, 25 Jan 2026 20:02:12 +0100 Subject: [PATCH 7/8] Remove unnecessary blank lines in espNowReceiveCB function --- wled00/udp.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 27acb65471..26a80ac349 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -952,8 +952,6 @@ void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rs return; } - - partial_packet_t *buffer = reinterpret_cast(data); if (len < 3 || !broadcast || buffer->magic != 'W' || !useESPNowSync || WLED_CONNECTED) { DEBUG_PRINTLN(F("ESP-NOW unexpected packet, not syncing or connected to WiFi.")); From 90fb15af99efc7621e461ff86f7210db2f0cdf57 Mon Sep 17 00:00:00 2001 From: spectrenoir06 Date: Sun, 25 Jan 2026 20:04:51 +0100 Subject: [PATCH 8/8] Fix initial value of lastProcessedMsgId to 255 for proper fragment handling --- usermods/espnow_json_handler/espnow_json_handler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/espnow_json_handler/espnow_json_handler.cpp b/usermods/espnow_json_handler/espnow_json_handler.cpp index efdfd9f4d9..4f9bf5a37f 100644 --- a/usermods/espnow_json_handler/espnow_json_handler.cpp +++ b/usermods/espnow_json_handler/espnow_json_handler.cpp @@ -24,7 +24,7 @@ class EspNowJsonHandler : public Usermod { private: // Fragment reassembly state uint8_t lastMsgId = 0; - uint8_t lastProcessedMsgId = 0; + uint8_t lastProcessedMsgId = 255; uint8_t fragmentsReceived = 0; uint8_t* reassemblyBuffer = nullptr; size_t reassemblySize = 0;