--- phase: 01-esp32-firmware plan: "02" subsystem: firmware tags: [esp32, arduinojson, asyncudp, freertos, udp, json, led, protocol] # Dependency graph requires: - phase: 01-esp32-firmware/01-01 provides: config.h (UDP_PORT, STACK_WIFI, PRIORITY_WIFI_RECV), led_driver.h, main.cpp WiFi scaffold provides: - LedCommand struct with CmdType, Zone, AnimParams for animation engine consumption - parseCommand() — ArduinoJson v7 JSON deserialization, zone routing, color parsing - buildStatusResponse() — JSON status reply builder - startUdpServer() — AsyncUDP listener with two-queue FreeRTOS pattern - commandQueue (extern QueueHandle_t) — channel from UDP layer to animation engine affects: - 01-esp32-firmware/01-03 (animation engine reads commandQueue and LedCommand structs) - 02-pi-transport (Pi-side must implement the exact JSON schema defined here) # Tech tracking tech-stack: added: - ArduinoJson 7.4.x (JsonDocument, deserializeJson, serializeJson) - AsyncUDP (arduino-esp32 stdlib, udp.listen, onPacket callback) patterns: - Two-queue pattern: AsyncUDP callback -> rawQueue (ISR-safe) -> parseTask -> commandQueue - Never parse JSON in AsyncUDP callback (heap allocation = watchdog reset risk) - STATUS command handled inline in parseTask (no animation engine involved) key-files: created: - firmware/src/protocol.h - firmware/src/protocol.cpp - firmware/src/wifi_server.h - firmware/src/wifi_server.cpp modified: - firmware/src/main.cpp key-decisions: - "Two-queue pattern (rawQueue -> parseTask -> commandQueue) to avoid JSON parsing in AsyncUDP callback (watchdog risk per RESEARCH Pattern 2)" - "STATUS cmd handled inline in parseTask: buildStatusResponse() sends UDP reply directly without routing through animation engine" - "Brightness value clamped to 0-60 in parseCommand() enforcing D-09 firmware cap" - "Zone is mandatory for ANIMATION/STOP/BRIGHTNESS but not STATUS (STATUS responds for all zones)" - "Default RGBW color [255,255,255,255] applied when params.colors absent in animation command" patterns-established: - "Pattern: Two-queue FreeRTOS — ISR-safe xQueueSendFromISR in callback, xQueueReceive + portMAX_DELAY in task" - "Pattern: parseCommand() returns bool — false = drop packet, log to Serial, no UDP error response (D-07)" - "Pattern: LedCommand carries senderIP/senderPort for STATUS replies" requirements-completed: [FW-02, FW-05] # Metrics duration: 3min completed: 2026-04-03 --- # Phase 01 Plan 02: UDP Command Receiver and JSON Protocol Parser Summary **AsyncUDP listener with two-queue FreeRTOS pattern — raw bytes via xQueueSendFromISR, JSON deserialized in parseTask, LedCommand structs dispatched to commandQueue for animation engine** ## Performance - **Duration:** 3 min - **Started:** 2026-04-03T11:22:48Z - **Completed:** 2026-04-03T11:25:43Z - **Tasks:** 2 - **Files modified:** 5 ## Accomplishments - Full protocol type system: LedCommand, AnimParams, Zone, CmdType enums with all fields from interface spec - parseCommand() implements version check (v:1), zone routing (schrank/wand/all), full animation params parsing including RGBW color arrays - wifi_server.cpp uses the two-queue pattern from RESEARCH.md Pattern 2: AsyncUDP callback never parses JSON, only copies bytes via xQueueSendFromISR - STATUS command handled immediately in parseTask with UDP reply to sender — no animation engine needed - main.cpp updated to call startUdpServer() after WIFI_PS_NONE ## Protocol Schema (for Phase 2 Pi-side reference) **UDP Port:** 4210 **Animation command:** ```json {"v":1, "zone":"schrank"|"wand"|"all", "animation":"chase", "params":{"speed":0.5, "intensity":1.0, "direction":0, "colors":[[255,0,0,0]]}} ``` **Stop command:** ```json {"v":1, "cmd":"stop", "zone":"schrank"|"wand"|"all"} ``` **Brightness command:** ```json {"v":1, "cmd":"brightness", "zone":"schrank"|"wand"|"all", "value":40} ``` Valid range: 0-60 (firmware clamps to 60 max per D-09). **Status command:** ```json {"v":1, "cmd":"status"} ``` Response: `{"v":1, "status":"ok", "wand":"chase", "schrank":"breathe", "brightness":40}` **Protocol rules:** - Every command must include `"v":1` version field (D-08) - Zone field mandatory for ANIMATION, STOP, BRIGHTNESS commands - Unknown extra fields silently ignored (D-07) - Malformed JSON logged to Serial, packet dropped — no UDP error response (D-07) - Colors array format: `[[R,G,B,W], ...]` (up to 8 RGBW tuples) - Default color when params.colors absent: `[255,255,255,255]` (warm white) ## commandQueue Location (for Plan 03) ```cpp // In firmware/src/wifi_server.h: extern QueueHandle_t commandQueue; // declare to consume // In firmware/src/wifi_server.cpp: QueueHandle_t commandQueue; // definition // Plan 03 includes: #include "wifi_server.h" // access commandQueue ``` Queue depth: 8 LedCommand structs. Plan 03 animation engine reads from this queue. ## Task Commits Each task was committed atomically: 1. **Task 1: protocol.h/.cpp — JSON parsing into LedCommand** - `3323a87` (feat) 2. **Task 2: wifi_server.h/.cpp — AsyncUDP listener with FreeRTOS queue** - `230be99` (feat) ## Files Created/Modified - `firmware/src/protocol.h` - LedCommand, AnimParams, Zone, CmdType types; parseCommand() and buildStatusResponse() declarations - `firmware/src/protocol.cpp` - parseCommand() implementation with ArduinoJson v7; buildStatusResponse() using serializeJson() - `firmware/src/wifi_server.h` - commandQueue extern declaration; startUdpServer() declaration - `firmware/src/wifi_server.cpp` - RawPacket struct; rawQueue + commandQueue creation; AsyncUDP callback (ISR-safe); parseTask FreeRTOS task - `firmware/src/main.cpp` - Added wifi_server.h include; startUdpServer() call after WIFI_PS_NONE ## Decisions Made - Two-queue pattern chosen: AsyncUDP callback only copies bytes via xQueueSendFromISR, separate parseTask handles JSON. This is mandatory — JSON heap allocation in WiFi task callback causes watchdog resets. - STATUS command fully handled in parseTask without routing through commandQueue — it needs sender IP/port from the raw packet context, and doesn't need the animation engine. - Brightness clamped in parseCommand() to enforce D-09 hardware safety cap (max 60%). - Zone field treated as optional only for STATUS command; required for all other commands with explicit Serial log and packet drop if missing. ## Deviations from Plan None — plan executed exactly as written. ## Issues Encountered - animation_engine.h/.cpp and animations/animation_base.h were present as untracked files (pre-created scaffold) and were included in Task 2's commit. These belong to Plan 03 scope but their presence does not harm Plan 02 — they will be naturally used in Plan 03. No functional impact on this plan's deliverables. ## Known Stubs - buildStatusResponse() in parseTask uses hardcoded "unknown"/"unknown"/40 for wand/schrank animation names and brightness. These will be wired to actual ZoneState in Plan 03 when the animation engine has live state. Plan 03 must update this call. ## Next Phase Readiness - Plan 03 (animation engine) can immediately consume commandQueue via `#include "wifi_server.h"` and `extern QueueHandle_t commandQueue` - LedCommand struct is the complete data contract — animation engine reads CmdType, Zone, animName, AnimParams - Protocol schema above is the definitive reference for Phase 2 Pi-side transport implementation --- *Phase: 01-esp32-firmware* *Completed: 2026-04-03*