From 530e9da957fb26d764d840c3a271c32196d0749b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 13:32:43 +0200 Subject: [PATCH] feat(01-04): wire all subsystems in main.cpp + protocol documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - initStrips() → initAnimationEngine() → WiFi → WIFI_PS_NONE → startUdpServer() order - loop() handles WiFi reconnect: re-enables WIFI_PS_NONE, restarts UDP server - Startup breathing animation fires during WiFi connect (D-11 compliance) - D-10 WiFi timeout: continues last animation on disconnect, UDP auto-restarts - docs/protocol.md: all 8 animations, all commands, zones, port 4210, error handling --- docs/protocol.md | 82 +++++++++++++++++++++++ firmware/src/main.cpp | 152 ++++++++++++++---------------------------- 2 files changed, 132 insertions(+), 102 deletions(-) create mode 100644 docs/protocol.md diff --git a/docs/protocol.md b/docs/protocol.md new file mode 100644 index 0000000..74032e2 --- /dev/null +++ b/docs/protocol.md @@ -0,0 +1,82 @@ +# ESP32 LED Controller — UDP Protocol Reference + +**Version:** 1 (`"v":1` required in every command) +**Transport:** UDP +**Port:** 4210 +**Max payload:** 512 bytes + +## Zones + +| Zone value | Strip | LEDs | Protocol | +|------------|-------|------|----------| +| `"schrank"` | WS2801 (Schrank, cabinet) | 160 | SPI RGB | +| `"wand"` | SK6812 (Wand, wall) | 300 | RMT RGBW | +| `"all"` | Both strips | 460 | — | + +## Commands + +### Run Animation + +```json +{"v":1,"zone":"wand","animation":"chase","params":{"speed":0.5,"intensity":1.0,"colors":[[255,0,0,0]]}} +``` + +Fields: +- `zone` (required): `"schrank"`, `"wand"`, or `"all"` +- `animation` (required): see Animation List below +- `params.speed` (optional, 0.0–1.0, default 0.5): animation tempo multiplier +- `params.intensity` (optional, 0.0–1.0, default 1.0): brightness multiplier +- `params.colors` (optional): array of `[R,G,B,W]` arrays (W=0 → auto-derived on RGBW strip) +- `params.direction` (optional, 0 or 1): forward/reverse + +### Stop Animation + +```json +{"v":1,"cmd":"stop","zone":"all"} +``` + +### Set Brightness + +```json +{"v":1,"cmd":"brightness","zone":"all","value":40} +``` + +`value`: 0–60 (firmware enforces max 60% for safety) + +### Query Status + +```json +{"v":1,"cmd":"status"} +``` + +Response (UDP to sender IP:port): + +```json +{"v":1,"status":"ok","wand":"chase","schrank":"breathe","brightness":40} +``` + +## Animation List + +| Name | Effect | Key params | +|------|--------|------------| +| `chase` | Moving window of lit LEDs along strip | speed, colors[0] | +| `pulse` | All LEDs pulse to sine wave | speed, colors[0] | +| `rainbow` | HSV rainbow sweeps across strip | speed | +| `strobe` | Rapid on/off flash | speed (flash rate), colors[0] | +| `color_wash` | Cross-fades through palette colors | speed, colors[] | +| `breathe` | Slow brightness inhale/exhale | speed, colors[0] | +| `sparkle` | Random pixels flash | intensity, colors[0] | +| `gradient_sweep` | Gradient between 2 colors sweeps | speed, colors[0..1] | + +## Error Handling + +- Unknown commands → silently ignored, logged to ESP32 serial console +- Malformed JSON → silently dropped, logged to serial console +- Wrong version (`"v"` != 1) → dropped +- No UDP error responses on error (check serial console for debugging) + +## Timing + +- Animation tick rate: 50fps (20ms) +- UDP → animation start latency: <50ms (queue drain on next tick) +- WiFi disconnect: last animation continues, UDP server restarts on reconnect diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index caa8db3..daaa471 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -3,117 +3,65 @@ #include #include "config.h" #include "led_driver.h" -#include "credentials.h" +#include "animation_engine.h" #include "wifi_server.h" +#include "credentials.h" -// --------------------------------------------------------------------------- -// Color constants for startup indicators -// --------------------------------------------------------------------------- -static const RgbwColor COLOR_STARTUP_GREEN = RgbwColor(0, 30, 0, 0); // Dim green: WiFi connecting -static const RgbwColor COLOR_WIFI_READY = RgbwColor(0, 0, 0, 20); // Dim white: WiFi connected -static const RgbwColor COLOR_LOOP_RED = RgbwColor(15, 0, 0, 0); // Dim red: loop toggle A -static const RgbwColor COLOR_LOOP_BLUE = RgbwColor(0, 0, 15, 0); // Dim blue: loop toggle B - -// --------------------------------------------------------------------------- -// State -// --------------------------------------------------------------------------- -static uint32_t lastLoopTick = 0; -static bool loopColorToggle = false; - -// --------------------------------------------------------------------------- -// WiFi connect with timeout -// --------------------------------------------------------------------------- -static bool connectWiFi() { - WiFi.begin(WIFI_SSID, WIFI_PASS); - - uint32_t startMs = millis(); - Serial.print("WiFi connecting"); - - while (WiFi.status() != WL_CONNECTED) { - if (millis() - startMs > 30000) { - Serial.println(); - Serial.println("WiFi timeout — check credentials.h"); - return false; - } - delay(500); - Serial.print("."); - } - - Serial.println(); - return true; -} - -// --------------------------------------------------------------------------- -// setup() -// --------------------------------------------------------------------------- void setup() { - // 1. Serial — wait up to 2s for monitor to attach (USB CDC) Serial.begin(115200); - uint32_t t0 = millis(); - while (!Serial && millis() - t0 < 2000) { delay(10); } + delay(500); // Brief wait for serial monitor to attach - Serial.println("=== LED Sync Studio — ESP32-C3 SuperMini ==="); - - // 2. Initialize LED strips (clears both, prints driver info) + // 1. Initialize LED strips (clears to black, confirms hardware present) initStrips(); - // 3. Startup indicator: dim green = WiFi connecting - setAllWand(COLOR_STARTUP_GREEN); - showBothStrips(); + // 2. Start animation engine early — startup breathing animation fires (per D-11) + // This runs on the FreeRTOS task, so breathing is visible during WiFi connect + initAnimationEngine(); - // 4. Connect to WiFi - if (!connectWiFi()) { - // Stay in error state — strip stays green, serial shows timeout - // Developer must check credentials.h and reflash - return; - } - - // 5. CRITICAL: Disable WiFi power save immediately after connect - // Must be called before any LED output to prevent RMT interrupt corruption - esp_wifi_set_ps(WIFI_PS_NONE); - Serial.println("WIFI_PS_NONE set — RMT coexistence mode active"); - - // 6. Start UDP server (must be after WiFi connect and WIFI_PS_NONE) - startUdpServer(); - - // 7. Print connection info - Serial.print("WiFi connected: "); - Serial.println(WiFi.localIP().toString()); - - // 8. WiFi connected indicator: dim white - setAllWand(COLOR_WIFI_READY); - showBothStrips(); - - lastLoopTick = millis(); -} - -// --------------------------------------------------------------------------- -// loop() — static color test: toggle strip every 5s to confirm responsiveness -// --------------------------------------------------------------------------- -void loop() { - uint32_t now = millis(); - - if (now - lastLoopTick >= 5000) { - lastLoopTick = now; - - if (loopColorToggle) { - setAllWand(COLOR_LOOP_RED); - } else { - setAllWand(COLOR_LOOP_BLUE); + // 3. Connect to WiFi + Serial.print("Connecting to WiFi"); + WiFi.begin(WIFI_SSID, WIFI_PASS); + uint32_t wifiStart = millis(); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + if (millis() - wifiStart > 30000) { + Serial.println("\nWiFi timeout — continuing without network"); + break; // Per D-10: continue running last animation on disconnect } - showBothStrips(); - - loopColorToggle = !loopColorToggle; - Serial.println("loop tick"); } + + if (WiFi.status() == WL_CONNECTED) { + // CRITICAL: disable power save immediately after connect (per Pitfall 1) + esp_wifi_set_ps(WIFI_PS_NONE); + Serial.println("\nWiFi connected: " + WiFi.localIP().toString()); + Serial.println("WIFI_PS_NONE set — RMT coexistence mode active"); + + // 4. Start UDP command server (only if WiFi is up) + startUdpServer(); + Serial.printf("UDP server listening on port %d\n", UDP_PORT); + } + + Serial.println("Firmware ready. Animations: chase, pulse, rainbow, strobe, color_wash, breathe, sparkle, gradient_sweep"); } -// --------------------------------------------------------------------------- -// COEXISTENCE TEST PROCEDURE (run on day 1 before Plan 02): -// 1. Flash firmware, open serial monitor at 115200 -// 2. Confirm: "WIFI_PS_NONE set" + IP address printed -// 3. From Pi/laptop: run `for i in $(seq 1 300); do echo '{"v":1,"cmd":"test"}' | nc -u 4210; sleep 0.1; done` -// 4. Observe SK6812 strip for 30 seconds during UDP burst -// 5. PASS: Zero visible flicker throughout -// 6. FAIL: Any flicker -> check DMA buffer size, confirm WIFI_PS_NONE, try core 2.0.17 fallback -// --------------------------------------------------------------------------- +void loop() { + // FreeRTOS tasks handle everything. + // loop() only monitors WiFi reconnection (per D-10). + static bool wasConnected = (WiFi.status() == WL_CONNECTED); + static uint32_t lastCheck = 0; + + if (millis() - lastCheck > 5000) { + lastCheck = millis(); + bool isConnected = (WiFi.status() == WL_CONNECTED); + if (!wasConnected && isConnected) { + // Reconnected — re-enable WIFI_PS_NONE and re-start UDP server + esp_wifi_set_ps(WIFI_PS_NONE); + startUdpServer(); + Serial.println("WiFi reconnected, UDP server restarted"); + } + wasConnected = isConnected; + } + + vTaskDelay(pdMS_TO_TICKS(100)); // yield to FreeRTOS tasks +}