--- phase: 03-communication-protocol plan: 01 subsystem: protocol tags: [udp, wled, protocol, animation, led, struct, asyncio, tdd] # Dependency graph requires: - phase: 02-audio-engine provides: "FastAPI app with lifespan pattern and app.state for shared resources" provides: - "WLED-compatible DRGB/DRGBW/DNRGB UDP frame encoders" - "Custom 0xAC animation command packet encoder/decoder (16 bytes)" - "UDPSender async transport wrapper (single shared socket)" - "SK6812Device.encode_animation_cmd — real 16-byte packets, not empty stub" - "WS2801Device.encode_animation_cmd — real 16-byte packets, not empty stub" affects: - lightsync/protocol/ (new package) - lightsync/devices/sk6812.py - lightsync/devices/ws2801.py # Tech tracking tech-stack: added: [] patterns: - "TDD RED-GREEN: failing tests committed first, implementation committed to pass them" - "Single shared asyncio DatagramTransport — one socket sends to N device addresses (no socket pool)" - "WLED DRGB/DRGBW: 2-byte header [type, timeout] + RGB/RGBW body per pixel" - "WLED DNRGB: 4-byte header [type, timeout, start_hi, start_lo] + RGB chunks per packet" - "struct.pack('>BBH') for big-endian DNRGB start index (critical per WLED spec)" - "struct.pack('>BBBBf') for 0xAC animation command header + float32 speed" - "_NullProtocol swallows OS UDP errors — fire-and-forget is correct for LED delivery" key-files: created: - lightsync/protocol/__init__.py - lightsync/protocol/drgb.py - lightsync/protocol/animation_cmd.py - lightsync/protocol/udp_sender.py - tests/__init__.py - tests/test_protocol.py modified: - lightsync/devices/sk6812.py - lightsync/devices/ws2801.py key-decisions: - "TDD approach: tests committed as RED before implementation — forces verification of test coverage" - "0xAC animation command marker chosen well above WLED 0x01-0x04 range — no firmware collision" - "UDPSender uses asyncio DatagramTransport over asyncudp library — no dependency needed, stdlib suffices" - "Device stubs delegate to encode_animation_cmd via import alias _encode_cmd — clean, no code duplication" - "pytest added as dev dependency via uv add --dev pytest — was missing from pyproject.toml" # Metrics duration: ~4min completed: 2026-04-06 --- # Phase 3 Plan 01: Protocol Encoders Summary **WLED-compatible UDP protocol layer implemented — DRGB/DRGBW/DNRGB frame encoders, custom 0xAC animation command packet with full roundtrip, async UDPSender transport, and SK6812/WS2801 device stubs replaced with real 16-byte packets — all tested with 27 passing tests** ## Performance - **Duration:** ~4 min - **Started:** 2026-04-06T21:38:29Z - **Completed:** 2026-04-06T21:42:37Z - **Tasks:** 2 (Task 1 TDD, Task 2 auto) - **Tests:** 27 passing ## Accomplishments ### Task 1: Protocol encoders — DRGB/DRGBW/DNRGB + animation command (TDD) RED phase: 27 failing tests committed covering all encoder behaviors including byte layout, MTU truncation, DNRGB multi-packet splitting, big-endian start index, animation roundtrip, flags byte, and defaults. GREEN phase: Implemented `lightsync/protocol/drgb.py` with: - `encode_drgb`: 2-byte header + 3-byte-per-pixel body, truncates at 490 px (MTU) - `encode_drgbw`: 2-byte header + 4-byte-per-pixel body, truncates at 367 px (MTU) - `encode_dnrgb_packets`: splits at 489 px/packet with big-endian start index via `struct.pack(">BBH")` Implemented `lightsync/protocol/animation_cmd.py` with: - `ANIMATION_IDS` and `ANIMATION_NAMES` dicts mapping 7 animation names to IDs 0-6 - `encode_animation_cmd`: 16-byte packet (0xAC marker, version, type ID, flags, speed float32, primary/secondary color, white, density) - `decode_animation_cmd`: full inverse — roundtrips correctly for all 7 types All 27 tests pass. ### Task 2: UDP sender + device stub completion Created `lightsync/protocol/udp_sender.py`: - `_NullProtocol` swallows OS-level send errors (UDP delivery is best-effort) - `UDPSender` opens single shared DatagramTransport at `local_addr=("0.0.0.0", 0)` - `send(payload, ip, port)` is synchronous fire-and-forget - `start()`/`stop()` async lifecycle methods for FastAPI lifespan integration - `is_running` property reflects transport state Updated `lightsync/devices/sk6812.py` and `ws2801.py`: - Added `from lightsync.protocol.animation_cmd import encode_animation_cmd as _encode_cmd` - Replaced `return b""` stub with `return _encode_cmd(animation, params)` - SK6812: `encode_animation_cmd("solid_color", {"color":[255,0,0], "white":128})` → `ac0100003f000000ff00000000008000` (16 bytes, starts 0xAC) - WS2801: `encode_animation_cmd("chase", {"color":[0,255,0], "speed":0.7})` → `ac0101003f33333300ff000000000000` (16 bytes, starts 0xAC) ## Task Commits 1. **Task 1 RED: Failing protocol tests** — `ffc4ef6` (test) 2. **Task 1 GREEN: WLED encoders + animation cmd** — `5e13da3` (feat) 3. **Task 2: UDP sender + device stubs** — `f17e77f` (feat) ## Files Created/Modified - `lightsync/protocol/__init__.py` — empty package init - `lightsync/protocol/drgb.py` — DRGB/DRGBW/DNRGB encoders with WLED constants - `lightsync/protocol/animation_cmd.py` — 0xAC animation command encoder/decoder - `lightsync/protocol/udp_sender.py` — async UDP transport wrapper - `tests/__init__.py` — test package init - `tests/test_protocol.py` — 27 tests for protocol correctness - `lightsync/devices/sk6812.py` — stub replaced with real encoding - `lightsync/devices/ws2801.py` — stub replaced with real encoding ## Decisions Made - **TDD approach applied**: Tests written and committed as RED before implementation, ensuring coverage is meaningful not retrofitted - **0xAC marker**: Well above WLED 0x01-0x04 range — firmware can dispatch on first byte without ambiguity - **No asyncudp library**: Python stdlib asyncio DatagramTransport is sufficient and avoids an extra dependency - **pytest added as dev dep**: Was missing from pyproject.toml, added via `uv add --dev pytest` ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 - Blocking] pytest not installed** - **Found during:** TDD RED phase (running tests) - **Issue:** `uv run python -m pytest` failed with "No module named pytest" - **Fix:** `uv add --dev pytest` — added to pyproject.toml [project.optional-dependencies] dev section - **Files modified:** pyproject.toml (by uv) - **Commit:** (included in uv lockfile update before ffc4ef6) ## Verification Results - `python -m pytest tests/test_protocol.py -v` — 27 passed in 0.04s - `from lightsync.protocol.drgb import encode_drgb, encode_drgbw, encode_dnrgb_packets` — imports OK - `from lightsync.protocol.udp_sender import UDPSender` — imports OK - SK6812Device.encode_animation_cmd returns 16-byte 0xAC packet — PASS - WS2801Device.encode_animation_cmd returns 16-byte 0xAC packet — PASS ## Known Stubs None — all planned stubs replaced with real implementations. The `encode_animation_cmd` stubs in SK6812Device and WS2801Device that returned `b""` are now wired to the real protocol encoder. ## Self-Check Files created/modified: - lightsync/protocol/__init__.py — FOUND - lightsync/protocol/drgb.py — FOUND - lightsync/protocol/animation_cmd.py — FOUND - lightsync/protocol/udp_sender.py — FOUND - tests/__init__.py — FOUND - tests/test_protocol.py — FOUND - lightsync/devices/sk6812.py — FOUND - lightsync/devices/ws2801.py — FOUND Commits: - ffc4ef6 — test(03-01) RED phase - 5e13da3 — feat(03-01) GREEN phase - f17e77f — feat(03-01) Task 2 ## Self-Check: PASSED --- *Phase: 03-communication-protocol* *Completed: 2026-04-06*