docs(phase-01): research ESP32-C3 firmware domain
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
648
.planning/phases/01-esp32-firmware/01-RESEARCH.md
Normal file
648
.planning/phases/01-esp32-firmware/01-RESEARCH.md
Normal file
@@ -0,0 +1,648 @@
|
||||
# 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>
|
||||
## 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.
|
||||
|
||||
</user_constraints>
|
||||
|
||||
---
|
||||
|
||||
<phase_requirements>
|
||||
## 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 |
|
||||
|
||||
</phase_requirements>
|
||||
|
||||
---
|
||||
|
||||
## 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<NeoGrbwFeature, NeoEsp32Rmt0Ws2812xMethod> 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 <NeoPixelBus.h>
|
||||
|
||||
// SK6812 RGBW via RMT channel 0
|
||||
// NeoGrbwFeature = 4 bytes/LED in GRBW order (SK6812 standard)
|
||||
NeoPixelBus<NeoGrbwFeature, NeoEsp32Rmt0Ws2812xMethod> stripWand(300, 3);
|
||||
// ^ GPIO3
|
||||
|
||||
// WS2801 RGB via hardware SPI at 2MHz (cable-safe)
|
||||
// NeoWs2801Spi2MhzMethod uses SPI.h internally with 2MHz clock
|
||||
NeoPixelBus<NeoRgbFeature, NeoWs2801Spi2MhzMethod> 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 <ArduinoJson.h>
|
||||
|
||||
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<JsonArray>();
|
||||
}
|
||||
```
|
||||
|
||||
### AsyncUDP Server Setup
|
||||
|
||||
```cpp
|
||||
// Source: arduino-esp32 AsyncUDP example
|
||||
#include <AsyncUDP.h>
|
||||
|
||||
#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<N>` 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<SK6812, DATA_PIN, GRBW>()` 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)
|
||||
Reference in New Issue
Block a user