-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Add JSON data handling over ESP-NOW in espNowReceiveCB #5161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f2797c0
124b9f9
e7e3999
3ae4c15
737dafc
222e028
1860e76
90fb15a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| #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 (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) | ||
| * - Byte 3+: JSON data payload | ||
| */ | ||
|
|
||
| #define ESPNOW_FRAGMENT_HEADER_SIZE 3 | ||
| #define ESPNOW_REASSEMBLY_TIMEOUT_MS 5000 // 5 second timeout for incomplete fragment reassembly | ||
|
|
||
| class EspNowJsonHandler : public Usermod { | ||
|
|
||
| private: | ||
| // Fragment reassembly state | ||
| uint8_t lastMsgId = 0; | ||
| uint8_t lastProcessedMsgId = 255; | ||
| 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 | ||
|
|
||
| /** | ||
| * 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); | ||
| reassemblyBuffer = nullptr; | ||
| } | ||
| fragmentsReceived = 0; | ||
| reassemblySize = 0; | ||
| reassemblyStartTime = 0; | ||
| } | ||
|
|
||
| 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) { | ||
| 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(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 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 { | ||
| 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) { | ||
| knownRemote = true; | ||
| break; | ||
| } | ||
| } | ||
| if (!knownRemote) { | ||
| DEBUG_PRINT(F("ESP Now Message Received from Unlinked Sender: ")); | ||
| DEBUG_PRINTLN(_last_signal_src); | ||
| return false; // Not handled | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // Need at least header size to process | ||
| if (len < ESPNOW_FRAGMENT_HEADER_SIZE) { | ||
| return false; // Not handled | ||
| } | ||
|
|
||
| // 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 | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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; | ||
| reassemblyStartTime = millis(); // Start timeout timer for new message | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // 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 | ||
| } | ||
|
Comment on lines
+142
to
+151
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add an upper bound on total reassembly buffer size to prevent memory exhaustion. A malicious or buggy sender could specify 🛠️ Suggested fixAdd a constant and check: `#define` ESPNOW_FRAGMENT_HEADER_SIZE 3
`#define` ESPNOW_REASSEMBLY_TIMEOUT_MS 5000
+#define ESPNOW_MAX_REASSEMBLY_SIZE 4096 // 4KB max JSON payload size_t fragmentDataSize = len - ESPNOW_FRAGMENT_HEADER_SIZE;
size_t newSize = reassemblySize + fragmentDataSize;
+ if (newSize > ESPNOW_MAX_REASSEMBLY_SIZE) {
+ DEBUG_PRINTLN(F("ESP-NOW fragment reassembly: max size exceeded"));
+ cleanupReassembly();
+ return true; // Handled by failing gracefully
+ }
+
uint8_t* newBuffer = (uint8_t*)realloc(reassemblyBuffer, newSize + 1);🤖 Prompt for AI Agents |
||
|
|
||
| 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<JsonObject>(); | ||
| 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 | ||
| } | ||
|
|
||
| /** | ||
| * Returns the unique identifier for this usermod | ||
| * @return USERMOD_ID_ESPNOW_JSON | ||
| */ | ||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "name": "espnow_json_handler", | ||
| "build": { "libArchive": false }, | ||
| "dependencies": {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
Uh oh!
There was an error while loading. Please reload this page.