# Phase 1: ESP32 Firmware - Research **Researched:** 2026-04-03 **Domain:** ESP32-C3 embedded firmware — dual LED strip control (WS2801 SPI + SK6812 RMT), UDP/JSON command reception, FreeRTOS animation engine **Confidence:** MEDIUM-HIGH (hardware-specific constraints require on-device validation for several items) --- ## User Constraints (from CONTEXT.md) ### Locked Decisions - **D-01:** Animations use a palette-based color system with full RGBW support. Each animation accepts a color palette (1-N colors as RGBW arrays) plus effect-specific parameters (speed, intensity, direction). - **D-02:** White channel (W) on SK6812 is auto-derived from RGB when not explicitly specified in the command. User can override W directly for warm/cool white effects. - **D-03:** 8 initial animations: Chase, Pulse, Rainbow, Strobe, Color Wash, Breathe, Sparkle, Gradient Sweep. All must work on both strip types. - **D-04:** Animation tick rate: 50fps (20ms per frame) as a FreeRTOS task. This is the animation engine update rate, not the LED refresh rate. - **D-05:** Flat JSON command structure: `{"zone":"schrank"|"wand"|"all", "animation":"chase", "params":{"speed":0.5, "intensity":1.0, "colors":[[255,0,128,0]]}}`. Zone field is mandatory. - **D-06:** Additional commands: `{"cmd":"stop","zone":"all"}`, `{"cmd":"brightness","zone":"all","value":100}`, `{"cmd":"status"}` (returns JSON via UDP response). - **D-07:** Unknown commands are ignored silently; errors logged to serial console for debugging. - **D-08:** Protocol includes a `"v":1` version field in every command for future compatibility. - **D-09:** Firmware-enforced brightness cap at 40% (configurable via JSON command, max 60%). Research flagged 27A peak at full brightness — dedicated 5V supplies per strip required. - **D-10:** On WiFi disconnect: continue running the last animation. On reconnect: accept new commands immediately. No auto-off timeout. - **D-11:** Startup behavior: boot with a subtle breathing animation (low brightness) to indicate firmware is alive and WiFi is connecting. - **D-12:** PlatformIO with Arduino framework. NeoPixelBus (RMT) for SK6812, hardware SPI for WS2801. - **D-13:** ArduinoJson v7 for JSON parsing, AsyncUDP for WiFi command reception. - **D-14:** Serial debug output for development. No OTA in Phase 1 — flash via USB. ### Claude's Discretion - FreeRTOS task priorities and stack sizes - Exact GPIO pin assignments (validate on hardware — ESP32-C3 SuperMini pinout constraints) - DMA buffer sizing for RMT to mitigate WiFi interrupt interference - WiFi reconnection strategy and timing ### Deferred Ideas (OUT OF SCOPE) None — discussion stayed within phase scope. --- ## Phase Requirements | ID | Description | Research Support | |----|-------------|------------------| | FW-01 | ESP32-C3 drives WS2801 (160 LEDs, SPI) and SK6812 (300 LEDs, RMT/RGBW) simultaneously | NeoPixelBus library handles both; RMT is non-blocking (DMA), SPI is blocking-fast — concurrent output pattern established | | FW-02 | ESP32-C3 receives JSON commands over UDP and executes named animations | AsyncUDP (built into arduino-esp32 core) + ArduinoJson v7 + FreeRTOS queue pattern documented | | FW-03 | At least 8 built-in animations (Chase, Pulse, Rainbow, Strobe, Color Wash, Breathe, Sparkle, Gradient Sweep) | Animation registry pattern documented; all 8 are 1D strip effects implementable with frame buffer approach | | FW-04 | Each animation accepts configurable parameters (color/RGBW, speed, intensity) | RGBW palette system + params struct pattern; ArduinoJson v7 extracts typed fields cleanly | | FW-05 | Both LED zones (Schrank/Wand) are independently controllable via JSON protocol | Zone field in protocol routes to independent strip state machines; concurrent RMT+SPI output confirmed | --- ## Summary Phase 1 builds a complete ESP32-C3 SuperMini firmware that drives two LED strips simultaneously (WS2801 via SPI, SK6812 via RMT), receives animation commands over UDP/JSON, and executes 8 named animations autonomously. The architecture is a three-task FreeRTOS system: a WiFi/UDP receiver that copies raw packets into a queue, an animation tick task that updates two independent frame buffers at 50fps, and an LED output task that drains the buffers to hardware. The most critical risk for this phase is the ESP32-C3 WiFi+RMT coexistence problem — a well-documented issue where WiFi interrupt activity (every ~1ms) corrupts single-wire LED timing on the single-core RISC-V chip. This must be tested and validated on day one before any animation logic is written. The mitigation strategy is: use NeoPixelBus RMT with DMA enabled and buffer sized to hold a full frame, plus `esp_wifi_set_ps(WIFI_PS_NONE)`. A secondary risk is NeoPixelBus compatibility with Arduino ESP32 core 3.x due to legacy RMT driver conflicts — the `platformio.ini` must include the workaround build flag. **Primary recommendation:** Validate WiFi+RMT coexistence on first flash with a static SK6812 frame + rapid UDP command bursts. If flickering occurs, confirm DMA buffer sizing and `WIFI_PS_NONE` before proceeding to animation code. --- ## Standard Stack ### Core | Library | Version | Purpose | Why Standard | |---------|---------|---------|--------------| | PlatformIO | latest | Build system, dependency management | CLI-driven, reproducible `platformio.ini` builds; far superior to Arduino IDE for this project. Manages NeoPixelBus + ArduinoJson versions explicitly. | | Arduino framework (espressif32) | 3.x | Hardware abstraction over ESP-IDF | Largest ESP32 library ecosystem; AsyncUDP is stdlib in this framework. Core 3.x is ESP-IDF 5 based. | | NeoPixelBus by Makuna | ^2.8.0 | SK6812 RMT drive + WS2801 SPI drive | Handles both strip protocols in one library. RMT method uses DMA, hardware-timed, no CPU spin. RGBW (4 bytes/LED) correctly supported for SK6812. | | ArduinoJson | ^7.4.0 | JSON deserialization of commands | v7 uses heap-allocated elastic documents — no manual buffer sizing. <1ms parse time for typical payloads. | | AsyncUDP | (arduino-esp32 stdlib) | UDP packet reception without blocking | Built into arduino-esp32 core, no external dep. Receives packets via callback, ISR-safe copy pattern needed. | ### Supporting | Library | Version | Purpose | When to Use | |---------|---------|---------|-------------| | FreeRTOS | (ESP-IDF stdlib) | Task scheduling, inter-task queues | Already available in Arduino ESP32. Use `xQueueCreate`/`xQueueSend` for UDP→animation data flow. | | Arduino SPI.h | (core stdlib) | WS2801 SPI output (fallback) | Only needed if NeoPixelBus `NeoWs2801SpiMethod` has issues. Prefer NeoPixelBus method. | ### Alternatives Considered | Instead of | Could Use | Tradeoff | |------------|-----------|----------| | NeoPixelBus (RMT) | FastLED | FastLED disables interrupts during WS2812 transfer — causes WiFi packet drops. FastLED ESP32-C3 RMT support is also less stable. | | NeoPixelBus (RMT) | Adafruit NeoPixel | Same interrupt-disable problem as FastLED. No RGBW SK6812 4-byte mode. | | ArduinoJson v7 | ArduinoJson v6 | v6 requires pre-sized `StaticJsonDocument` — fragile with variable-length color arrays. v7 elastic docs are safer. | | AsyncUDP | ESPAsyncWebServer | ESPAsyncWebServer is archived (January 2025), has crash bugs on single-core C3. | | AsyncUDP | plain WiFiUDP | WiFiUDP is blocking — must poll in a loop. AsyncUDP uses callback, cleaner with FreeRTOS. | **Installation (platformio.ini):** ```ini [env:esp32c3_supermini] platform = espressif32 board = esp32-c3-devkitm-1 framework = arduino monitor_speed = 115200 build_flags = -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 -DESP32_ARDUINO_NO_RGB_BUILTIN lib_deps = makuna/NeoPixelBus @ ^2.8.0 bblanchon/ArduinoJson @ ^7.4.0 ``` The `-DESP32_ARDUINO_NO_RGB_BUILTIN` flag is mandatory — it prevents the Arduino core from linking `neopixelWrite()` (new RMT driver) alongside NeoPixelBus (legacy RMT driver), which causes a fatal startup conflict: `CONFLICT! driver_ng is not allowed to be used with the legacy driver`. **Note on board name:** `esp32-c3-devkitm-1` is the closest standard PlatformIO board for the SuperMini. The `nologo_esp32c3_super_mini` board variant exists in newer platform versions. Either works; `esp32-c3-devkitm-1` is more widely tested. --- ## Architecture Patterns ### Recommended Project Structure ``` firmware/ ├── platformio.ini ├── src/ │ ├── main.cpp # Setup + WiFi init + task launch │ ├── config.h # Pin defs, constants, brightness cap │ ├── led_driver.h/.cpp # NeoPixelBus strip objects, show() │ ├── animation_engine.h/.cpp # 50fps tick, zone state machines │ ├── animations/ │ │ ├── animation_base.h # Abstract base: tick(t, params, buffer) │ │ ├── chase.cpp │ │ ├── pulse.cpp │ │ ├── rainbow.cpp │ │ ├── strobe.cpp │ │ ├── color_wash.cpp │ │ ├── breathe.cpp │ │ ├── sparkle.cpp │ │ └── gradient_sweep.cpp │ ├── protocol.h/.cpp # JSON parse, command struct, response │ └── wifi_server.h/.cpp # AsyncUDP listener, queue send └── test/ # (optional: Unity unit tests for animation math) ``` ### Pattern 1: Three-Task FreeRTOS Architecture **What:** Split firmware into three FreeRTOS tasks with explicit priority ordering and queue-based communication. **When to use:** Any firmware that must receive network data while simultaneously outputting to hardware at a fixed tick rate. ```cpp // Source: ESP32 FreeRTOS documentation + architecture research // Task priorities (higher number = higher priority on single core) #define PRIORITY_LED_OUTPUT 3 // Highest: LED timing is critical #define PRIORITY_ANIM_TICK 2 // Medium: 50fps tick #define PRIORITY_WIFI_RECV 1 // Lowest: network I/O can wait // Stack sizes (in words on ESP32 = bytes/4 on most compilers, but // xTaskCreate uses words on ESP-IDF / bytes on Arduino abstraction) // Use 4096 bytes for animation task (JSON heap alloc needs headroom) // Use 8192 bytes for WiFi task (TLS/TCP stack even for UDP) #define STACK_WIFI 8192 #define STACK_ANIM 4096 #define STACK_LED 2048 // Command queue: UDP → Animation Engine QueueHandle_t commandQueue; void setup() { commandQueue = xQueueCreate(8, sizeof(LedCommand)); xTaskCreate(wifiTask, "wifi", STACK_WIFI, NULL, PRIORITY_WIFI_RECV, NULL); xTaskCreate(animTickTask, "anim", STACK_ANIM, NULL, PRIORITY_ANIM_TICK, NULL); xTaskCreate(ledOutputTask,"led", STACK_LED, NULL, PRIORITY_LED_OUTPUT, NULL); } ``` **Why LED task has highest priority:** On single-core C3, if the LED output task wakes for a show() call but gets preempted by WiFi, the timing gap causes LED corruption. Keeping LED output highest ensures it completes without interruption. ### Pattern 2: AsyncUDP → FreeRTOS Queue (Never Parse in Callback) **What:** AsyncUDP callback copies raw packet bytes into a FreeRTOS queue. A separate task drains the queue and parses JSON. **When to use:** Always — JSON parsing (heap allocation, string iteration) inside a network callback causes watchdog resets. ```cpp // Source: Arduino ESP32 AsyncUDP example + ArduinoJson FreeRTOS docs AsyncUDP udp; struct RawPacket { uint8_t data[512]; size_t len; }; QueueHandle_t rawQueue; void setupUDP() { rawQueue = xQueueCreate(4, sizeof(RawPacket)); if (udp.listen(UDP_PORT)) { udp.onPacket([](AsyncUDPPacket packet) { RawPacket raw; raw.len = min(packet.length(), (size_t)512); memcpy(raw.data, packet.data(), raw.len); // ISR-safe send (timeout=0, don't block in callback) xQueueSendFromISR(rawQueue, &raw, NULL); }); } } void parseTask(void* pvParams) { RawPacket raw; while (true) { if (xQueueReceive(rawQueue, &raw, portMAX_DELAY)) { JsonDocument doc; DeserializationError err = deserializeJson(doc, raw.data, raw.len); if (err) { Serial.println(err.c_str()); continue; } // ... extract command and send to animation engine } } } ``` ### Pattern 3: Animation Registry with Function Pointers **What:** Register animations by string name in a `std::map` (or flat array) at boot. The command handler looks up the name and sets a function pointer on the zone's state. **When to use:** Any firmware with multiple named animation modes. ```cpp // Source: firmware architecture decision struct AnimParams { uint8_t colors[8][4]; // up to 8 RGBW colors uint8_t colorCount; float speed; float intensity; uint8_t direction; }; typedef void (*AnimTickFn)(uint32_t tick, const AnimParams& p, uint8_t* frameBuffer, uint16_t ledCount); struct AnimEntry { const char* name; AnimTickFn fn; }; const AnimEntry ANIM_REGISTRY[] = { {"chase", animChase}, {"pulse", animPulse}, {"rainbow", animRainbow}, {"strobe", animStrobe}, {"color_wash", animColorWash}, {"breathe", animBreathe}, {"sparkle", animSparkle}, {"gradient_sweep", animGradientSweep}, }; ``` ### Pattern 4: Concurrent LED Output (RMT + SPI) **What:** Start RMT transfer (non-blocking DMA), then write SPI (blocking, fast), return. Both strips update in roughly the same time window. **When to use:** Every animation tick — this is the LED output pattern for the two-strip architecture. ```cpp // Source: ESP32-C3 RMT and SPI peripheral docs // RMT strip (SK6812, non-blocking DMA) stripWand.Show(); // queues DMA transfer, returns immediately // SPI strip (WS2801, blocking but fast) // 160 LEDs * 3 bytes = 480 bytes at 1MHz = ~4ms stripSchrank.Show(); // blocks ~4ms // RMT transfer is long enough (300*4 bytes at 800Kbps = ~9.6ms) // that both strips finish in about the same window. ``` ### Pattern 5: Brightness Cap Enforcement **What:** Scale all RGBW values by a cap factor before writing to the NeoPixelBus strip buffer. Never write full values. ```cpp // config.h constexpr float BRIGHTNESS_CAP = 0.40f; // 40% hardware max constexpr float BRIGHTNESS_MAX = 0.60f; // 60% user-settable ceiling float g_brightness = BRIGHTNESS_CAP; // current global brightness // In frame output: RgbwColor applyBrightness(RgbwColor c, float factor) { return RgbwColor(c.R * factor, c.G * factor, c.B * factor, c.W * factor); } ``` ### Anti-Patterns to Avoid - **JSON parsing in AsyncUDP callback:** Causes watchdog reset. Always copy bytes to queue first. - **`xTaskCreatePinnedToCore(fn, ..., 0)` or `..., 1)`:** On ESP32-C3 (single-core), core pinning is accepted without error but silently does nothing. Do not rely on it. - **Forgetting `WIFI_PS_NONE`:** WiFi power save mode triggers ~50ms beacon sleep cycles that cause RMT timing corruption. Must be disabled on boot. - **Default SPI speed for WS2801:** Default is 10MHz; WS2801 spec supports 25MHz but long cable runs require 1-2MHz to prevent bit errors at the far end. - **RGB instead of RGBW for SK6812:** Using `NeoRgbFeature` instead of `NeoGrbwFeature` sends 3 bytes/LED instead of 4, corrupting all LEDs after the first miscounted position. --- ## Don't Hand-Roll | Problem | Don't Build | Use Instead | Why | |---------|-------------|-------------|-----| | Single-wire LED timing (SK6812) | Custom bit-bang with `delayMicroseconds()` | NeoPixelBus RMT method | Bit-bang disables interrupts, guaranteed WiFi packet loss. RMT uses DMA, hardware-timed, interrupt-safe. | | SPI LED output (WS2801) | Raw `SPI.transfer()` loop with manual clock | NeoPixelBus `NeoWs2801SpiMethod` | NeoPixelBus handles SPI init, clock speed selection, and the latch timing gap automatically. `NeoWs2801Spi2MhzMethod` exists for cable-safe 2MHz operation. | | JSON parsing | Manual `strstr()`/`atoi()` parsing | ArduinoJson v7 `deserializeJson()` | JSON has edge cases (escaped chars, nested arrays, null values) that break naive parsers. ArduinoJson handles all of them. | | UDP receive | Polling `WiFiUDP.parsePacket()` in loop() | AsyncUDP with `onPacket()` callback | Polling in `loop()` misses packets during LED output. AsyncUDP callback fires from WiFi task, independent of `loop()`. | | Animation timing | `delay(20)` in animation loop | FreeRTOS `vTaskDelayUntil()` | `delay()` is cumulative drift. `vTaskDelayUntil(&lastWake, pdMS_TO_TICKS(20))` self-corrects for task execution time, giving exact 50fps. | | Color interpolation | Manual lerp with `map()` | Inline float lerp or NeoPixelBus `RgbwColor` blend | NeoPixelBus provides `RgbwColor::LinearBlend()` for smooth gradient math. | **Key insight:** The ESP32 peripheral hardware (RMT, SPI DMA) exists precisely to handle the timing requirements that are impossible to achieve correctly in software on an interrupt-heavy single-core system. Always delegate timing to hardware. --- ## Runtime State Inventory Step 2.5: SKIPPED — this is a greenfield firmware project with no rename/refactor scope. No runtime state categories apply. --- ## Environment Availability | Dependency | Required By | Available | Version | Fallback | |------------|------------|-----------|---------|----------| | PlatformIO CLI | Build + flash ESP32 | Unknown — not on this machine (VPS) | — | Install on developer's local machine; this VPS is not the build target | | Python 3 | PlatformIO internals | Likely (VPS has Python) | — | Install via apt | | USB serial (physical) | Flash ESP32 via USB | Not applicable on VPS | — | Developer's local machine has USB port | | ESP32-C3 SuperMini hardware | All firmware testing | Physical hardware only | — | No emulator substitute — Wokwi can simulate limited functionality | | 5V power supplies (dedicated) | LED strip testing | Physical hardware only | — | Cannot test strip power without hardware | **Note:** Firmware development and flashing happen on the developer's local machine with physical hardware, not on the groll.cloud VPS. The VPS hosts other project components (Pi-side app in later phases). This distinction means environment availability for Phase 1 is entirely hardware-dependent and cannot be verified remotely. **Missing dependencies with no fallback:** - Physical ESP32-C3 SuperMini hardware - Dedicated 5V power supplies (one per strip) - Local machine with PlatformIO installed and USB access --- ## Common Pitfalls ### Pitfall 1: WiFi Interrupts Corrupt RMT/SK6812 Timing (CRITICAL) **What goes wrong:** On the single-core ESP32-C3, WiFi's ~1ms interrupt cycle inserts ~5µs gaps into the RMT signal stream. SK6812 requires timing tolerances of ±150ns. This causes visible flickering correlated with network activity. **Why it happens:** Single core. No ability to pin WiFi to core 0 and LED to core 1 (the standard ESP32 mitigation). RMT DMA buffers that are too small cause mid-frame refills — each refill is a WiFi-interruptible gap. **How to avoid:** 1. Set `esp_wifi_set_ps(WIFI_PS_NONE)` in `setup()` — eliminates beacon sleep cycle spikes 2. Size the RMT DMA buffer to hold the full SK6812 frame: 300 LEDs × 4 bytes × 8 bits × 2 RMT symbols/bit = 19,200 RMT symbols minimum. Let NeoPixelBus size this automatically via its `ShowDma` method. 3. Test with WiFi transmitting at max rate (send UDP packets rapidly from Pi) while showing a static color — stability here means the mitigation works. **Warning signs:** LEDs stable with WiFi idle, flickering during UDP bursts. Flicker at a fixed ~1ms period. **Sources:** FastLED issue #1657, ESP32 forum thread on RMT NeoPixels + WiFi (confirmed February 2026) --- ### Pitfall 2: NeoPixelBus Legacy RMT Driver Conflict with Arduino Core 3.x **What goes wrong:** Arduino ESP32 core 3.x (ESP-IDF 5) includes a new RMT driver (`driver_ng`). NeoPixelBus 2.8.x uses the legacy RMT driver. If both are linked, ESP-IDF aborts at boot: `CONFLICT! driver_ng is not allowed to be used with the legacy driver`. **Why it happens:** The Arduino core's `RGB_BUILTIN` macro causes `digitalWrite()` to call `neopixelWrite()`, which links `driver_ng`. NeoPixelBus simultaneously links the legacy driver. Mutual exclusion causes fatal abort. **How to avoid:** Add `-DESP32_ARDUINO_NO_RGB_BUILTIN` to `build_flags` in `platformio.ini`. This suppresses the `RGB_BUILTIN` macro and breaks the driver conflict. **Alternative if NeoPixelBus RMT still fails:** Pin to Arduino ESP32 core 2.0.17 (`platform_packages = framework-arduinoespressif32 @ 2.0.17`) — this uses ESP-IDF 4 which has only the legacy RMT driver. **Warning signs:** `CONFLICT! driver_ng` in serial monitor. Firmware boots, LEDs show garbage or nothing. --- ### Pitfall 3: WS2801 Default SPI Speed Too High for Long Cable Run **What goes wrong:** Default SPI on ESP32 is 10MHz. WS2801 supports up to 25MHz in spec, but 5m cable (effectively 10m for clock signal round-trip) adds capacitive load, smearing clock edges. Colors corrupt at far-end LEDs. **How to avoid:** Use `NeoWs2801Spi2MhzMethod` (NeoPixelBus built-in 2MHz SPI method) instead of `NeoWs2801SpiMethod`. Add a 33Ω series resistor on MOSI and CLK at the ESP32 end. **Warning signs:** First N LEDs show correct color, remainder show wrong or static color. Errors worsen with longer cable or higher SPI speed. --- ### Pitfall 4: SK6812 Configured as RGB Instead of RGBW **What goes wrong:** Using `NeoRgbFeature` (3 bytes/LED) instead of `NeoGrbwFeature` (4 bytes/LED) for SK6812. Every LED after position 0 receives shifted byte data, producing wrong colors throughout the strip. **How to avoid:** Explicitly declare: ```cpp NeoPixelBus stripWand(300, PIN_SK6812); ``` Never use `NeoRgbFeature` or `NeoGrbFeature` for SK6812. **Detection:** Set LED 0 to pure red (255,0,0,0). If LED 1 shows any glow, byte framing is wrong. --- ### Pitfall 5: `vTaskDelay(20)` Drifts; Use `vTaskDelayUntil()` for 50fps **What goes wrong:** `vTaskDelay(pdMS_TO_TICKS(20))` sleeps for 20ms minimum, but does not account for the time spent computing the animation frame. Over time, the tick rate slowly drops below 50fps. **How to avoid:** ```cpp void animTickTask(void* pvParams) { TickType_t lastWake = xTaskGetTickCount(); while (true) { updateAnimations(); // compute frame pushToLedTask(); // signal LED output vTaskDelayUntil(&lastWake, pdMS_TO_TICKS(20)); // exact 50fps } } ``` --- ### Pitfall 6: ESP32-C3 SuperMini GPIO Constraints **What goes wrong:** Several GPIOs on the SuperMini are restricted. Using them causes boot failures, flash corruption, or USB enumeration failures. **Avoid:** - GPIO2, GPIO8, GPIO9 — strapping pins; state at boot affects boot mode - GPIO4-GPIO7 — JTAG pins; some sources also list these as flash control on certain board revisions - GPIO12-GPIO17 — internal flash (not broken out on SuperMini) - GPIO20-GPIO21 — UART0 (USB-to-serial for flashing) - GPIO8 — also connected to onboard blue LED (inverted) **Safe GPIO for peripherals:** GPIO0, GPIO1, GPIO3, GPIO10 are confirmed safe. GPIO5, GPIO6, GPIO7 usable for SPI on some board revisions — verify with oscilloscope. **Recommended pin assignment (validate on hardware):** - SK6812 RMT data: **GPIO3** (safe, RMT channel 0) - WS2801 MOSI: **GPIO7**, WS2801 CLK: **GPIO6** (hardware SPI) - Or: WS2801 MOSI: **GPIO1**, WS2801 CLK: **GPIO0** (if JTAG conflict confirmed on 6/7) --- ### Pitfall 7: Power Brown-Out Resets Mid-Animation **What goes wrong:** 300 SK6812 LEDs at 60mA each = 18A peak. 160 WS2801 at 60mA = 9.6A. Combined ~27A. Even at 40% brightness cap (7A), a single USB supply or shared 5V rail causes voltage droop below ESP32's brown-out threshold (~3.3V), causing resets. **How to avoid:** Dedicated 5V power supply per strip (not shared with ESP32 power). 1000µF cap at each strip's 5V input. Firmware brightness cap at 40%. Separate GND wire run. **Detection:** ESP32 resets with `rst:0x10 (RTCWDT_RTC_RESET)` or `rst:0x1 (POWERON_RESET)` during bright white animations — classic brown-out signature. --- ## Code Examples ### Minimal NeoPixelBus Setup for Both Strips ```cpp // Source: NeoPixelBus library docs + ESP32-C3 constraints #include // SK6812 RGBW via RMT channel 0 // NeoGrbwFeature = 4 bytes/LED in GRBW order (SK6812 standard) NeoPixelBus stripWand(300, 3); // ^ GPIO3 // WS2801 RGB via hardware SPI at 2MHz (cable-safe) // NeoWs2801Spi2MhzMethod uses SPI.h internally with 2MHz clock NeoPixelBus stripSchrank(160); // No data pin arg — uses default hardware SPI MOSI/CLK pins void setup() { esp_wifi_set_ps(WIFI_PS_NONE); // MUST be called after WiFi.begin() stripWand.Begin(); stripSchrank.Begin(); stripWand.Show(); // Clear to black stripSchrank.Show(); } ``` ### ArduinoJson v7 Command Parsing ```cpp // Source: ArduinoJson v7 documentation (bblanchon/arduinojson) #include void handleCommand(const uint8_t* data, size_t len) { JsonDocument doc; // v7: no size parameter needed DeserializationError err = deserializeJson(doc, data, len); if (err) { Serial.printf("JSON err: %s\n", err.c_str()); return; } int version = doc["v"] | 0; const char* zone = doc["zone"] | ""; const char* cmd = doc["cmd"] | ""; const char* anim = doc["animation"] | ""; // Access params sub-object JsonObject params = doc["params"]; float speed = params["speed"] | 1.0f; float intensity = params["intensity"] | 1.0f; // Colors: [[255,0,128,0], ...] JsonArray colors = params["colors"].as(); } ``` ### AsyncUDP Server Setup ```cpp // Source: arduino-esp32 AsyncUDP example #include #define UDP_PORT 4210 AsyncUDP udp; QueueHandle_t rawQueue; // raw bytes queue void setupUDP() { rawQueue = xQueueCreate(8, sizeof(RawPacket)); if (!udp.listen(UDP_PORT)) { Serial.println("UDP listen failed"); return; } udp.onPacket([](AsyncUDPPacket pkt) { RawPacket raw; raw.len = min((size_t)512, pkt.length()); memcpy(raw.data, pkt.data(), raw.len); xQueueSendFromISR(rawQueue, &raw, nullptr); }); } ``` ### WiFi Reconnection Strategy ```cpp // Source: Claude's discretion — standard ESP32 WiFi reconnect pattern void wifiTask(void* pvParams) { WiFi.begin(SSID, PASSWORD); WiFi.setAutoReconnect(true); // ESP32 handles reconnect automatically WiFi.persistent(false); // Don't write credentials to flash every time while (true) { if (WiFi.status() == WL_CONNECTED) { // Accept new commands — UDP listener runs separately } // D-10: On disconnect, animation continues from last command. // No action needed here — animation task is independent. vTaskDelay(pdMS_TO_TICKS(5000)); // Check every 5s } } ``` ### Startup Breathing Animation (D-11) ```cpp // Boot sequence: breathe at low brightness while WiFi connects void bootBreath(uint32_t tick) { float t = (tick % 100) / 100.0f; // 0-1 over 2 seconds at 50fps float brightness = 0.5f * (1.0f - cosf(t * 2 * PI)) * 0.15f; // 15% max RgbwColor warm(brightness*255, brightness*128, 0, brightness*200); for (int i = 0; i < 300; i++) stripWand.SetPixelColor(i, warm); for (int i = 0; i < 160; i++) stripSchrank.SetPixelColor(i, RgbColor(brightness*255, brightness*128, 0)); } ``` --- ## State of the Art | Old Approach | Current Approach | When Changed | Impact | |--------------|------------------|--------------|--------| | FastLED with interrupt-disable for ESP32 | NeoPixelBus RMT (DMA-based) | 2020+ | No CPU involvement during LED transfer; WiFi-safe | | `StaticJsonDocument` with fixed size | `JsonDocument` (elastic heap) | ArduinoJson v7 (2024) | No buffer overflow on variable-length color arrays | | ESPAsyncWebServer for UDP reception | Built-in `AsyncUDP` | 2025 (ESPAsyncWebServer archived) | Fewer dependencies, more stable on C3 | | Dual-core WiFi/LED isolation | Single-core with WIFI_PS_NONE + DMA buffers | ESP32-C3 era | Core pinning does not exist on C3 — different mitigation required | | NeoWs2801SpiMethod (10MHz default) | NeoWs2801Spi2MhzMethod (2MHz safe) | NeoPixelBus 2.x | Prevents cable-induced bit errors on long WS2801 runs | **Deprecated/outdated:** - `FastLED.addLeds()` on ESP32-C3: Interrupt-disable transfer breaks WiFi; C3 RMT support in FastLED is less stable than NeoPixelBus as of early 2026. - `StaticJsonDocument<256>` (ArduinoJson v6 pattern): Replaced by elastic `JsonDocument` in v7. - ESPAsyncWebServer (me-no-dev): Archived January 2025. Use AsyncUDP directly. - Core pinning `xTaskCreatePinnedToCore(..., core_id)` on ESP32-C3: Accepted by compiler, silently ignored at runtime. --- ## Open Questions 1. **NeoPixelBus 2.8.x with Arduino Core 3.x on physical hardware** - What we know: There is a known RMT legacy/ng conflict. The `-DESP32_ARDUINO_NO_RGB_BUILTIN` flag is the documented workaround. NeoPixelBus has a CORE3 branch in development. - What's unclear: Whether 2.8.x + the build flag actually produces stable RMT output on real ESP32-C3 hardware, or whether downgrading to core 2.0.17 is required in practice. - Recommendation: First task in Wave 1 should be a minimal "blink test" that flashes both strips while sending UDP commands. If RMT output is wrong or absent, downgrade to core 2.0.17 via `platform_packages` override. 2. **GPIO pin assignment for simultaneous SPI + RMT on SuperMini** - What we know: GPIO3 is safe for RMT. GPIO6/GPIO7 are listed as SPI by some sources, but also flagged as JTAG pins by others. Conflicts need hardware-level testing. - What's unclear: Whether GPIO6/GPIO7 conflict with JTAG usage in this specific board revision, or whether the JTAG concern is theoretical on production firmware. - Recommendation: Test GPIO6 (CLK) + GPIO7 (MOSI) for WS2801 first. If SPI output is corrupted, fall back to GPIO0 (CLK) + GPIO1 (MOSI). 3. **RMT DMA buffer size for 300 LED SK6812 frame** - What we know: NeoPixelBus auto-sizes the buffer. RMT buffer = same size as pixel buffer. For 300 LEDs × 4 bytes × 8 bits × 2 RMT symbols = ~19,200 symbols. - What's unclear: Whether NeoPixelBus 2.8.x correctly allocates a large enough DMA buffer for 300 LEDs to prevent mid-frame refills on ESP32-C3. - Recommendation: Measure RMT transfer time with oscilloscope or logic analyzer on first flash. If gaps appear mid-frame, file issue with NeoPixelBus or use workaround from Issue #598. 4. **AsyncUDP response path for `{"cmd":"status"}` queries** - What we know: AsyncUDP provides `packet.print()` / `packet.write()` for responses. The callback receives the sender's IP/port. - What's unclear: Whether `xQueueSendFromISR` is safe in the AsyncUDP callback, or if AsyncUDP callbacks run in a FreeRTOS task context (making regular `xQueueSend` fine). - Recommendation: Check if `AsyncUDP::onPacket` fires in task context (not ISR). If task context, use `xQueueSend` with timeout=0. Test with `uxTaskGetStackHighWaterMark()` to ensure parse task stack doesn't overflow. --- ## Sources ### Primary (HIGH confidence) - NeoPixelBus GitHub Wiki — ESP32 NeoMethods: https://github.com/Makuna/NeoPixelBus/wiki/ESP32-NeoMethods - NeoPixelBus Issue #815 — Legacy RMT driver conflict with Arduino core 3.x: https://github.com/Makuna/NeoPixelBus/issues/815 - NeoPixelBus Issue #324 — WS2801 SPI support confirmed: https://github.com/Makuna/NeoPixelBus/issues/324 - ArduinoJson v7 documentation (bblanchon): verified via project research STACK.md - arduino-esp32 AsyncUDP example: https://github.com/espressif/arduino-esp32/blob/master/libraries/AsyncUDP/examples/AsyncUDPServer/AsyncUDPServer.ino - FastLED Issue #1657 — ESP32-C3 WiFi+RMT flicker (active February 2026): https://github.com/FastLED/FastLED/issues/1657 ### Secondary (MEDIUM confidence) - NeoPixelBus Discussion #822 — RMT on ESP32-C3 Super Mini (core 2.0.17 workaround): https://github.com/Makuna/NeoPixelBus/discussions/822 - ESP32 forum — RMT NeoPixels flicker with WiFi: https://esp32.com/viewtopic.php?t=3980 - espboards.dev — ESP32-C3 SuperMini pinout: https://www.espboards.dev/esp32/esp32-c3-super-mini/ - PlatformIO Community — ESP32-C3 SuperMini config: https://community.platformio.org/t/esp32-c3-supermini-config/47117 - arduino-esp32 PR #9941 — RMT legacy driver workaround for core 3.x: https://github.com/espressif/arduino-esp32/pull/9941 ### Tertiary (LOW confidence — flag for hardware validation) - GPIO6/GPIO7 JTAG conflict claim: multiple forum sources, not confirmed in official Espressif docs for this board variant - RMT DMA auto-sizing adequacy for 300 LED SK6812: inferred from NeoPixelBus source, not tested on physical hardware --- ## Metadata **Confidence breakdown:** - Standard stack: HIGH — NeoPixelBus, ArduinoJson v7, AsyncUDP all confirmed in official sources. Build flag workaround confirmed in arduino-esp32 issue tracker. - Architecture: HIGH — FreeRTOS three-task pattern is standard ESP32 practice; concurrent RMT+SPI output pattern confirmed in ESP32 documentation. - GPIO pin assignments: MEDIUM — "safe" GPIOs confirmed, specific SPI pin conflicts require hardware validation. - WiFi+RMT coexistence: MEDIUM — mitigation documented, effectiveness on physical hardware requires day-one validation. - Pitfalls: HIGH — all major pitfalls sourced from official issue trackers and forum reports with reproducible examples. **Research date:** 2026-04-03 **Valid until:** 2026-05-03 (30 days — NeoPixelBus CORE3 branch may release updated RMT support; check repo before starting)