--- phase: 02-app-core-audio plan: "04" subsystem: scheduling tags: [asyncio, monotonic-clock, choreography, timing, scheduler] # Dependency graph requires: - phase: 02-01 provides: ChoreoEvent Pydantic model with timestamp/zone/animation/params fields - phase: 02-02 provides: ESP32Transport.send_command() for fire-and-forget UDP dispatch provides: - ChoreographyScheduler asyncio task — absolute monotonic clock event dispatch - pause/resume with pause_offset accumulation (D-09) - seek() skips events before seek position - on_event callback hook for TUI integration affects: - 02-05 (beat detection integration) - 03-xx (TUI wraps scheduler for playback controls) - phase 4 (live reactive mode uses on_event hook) # Tech tracking tech-stack: added: [] patterns: - "Absolute monotonic scheduling: fire_at = start_time + pause_offset + event.timestamp — never accumulate relative delays" - "5ms poll interval (asyncio.sleep(min(remaining, 0.005))) keeps event loop responsive while maintaining <1ms drift" - "pause_offset accumulation for correct pause/resume without event skipping or double-firing" key-files: created: - src/led_sync/scheduler.py - tests/test_scheduler.py modified: [] key-decisions: - "Absolute monotonic timestamps (D-07): fire_at recalculated fresh each 5ms poll iteration — mathematical guarantee of <1ms drift regardless of duration" - "pause_offset accumulation (D-09): resume() adds elapsed pause time to offset, keeping all future fire_at calculations correct" - "Scheduler does not inject v:1 — that is transport's responsibility, scheduler sends {zone, animation, params} only" patterns-established: - "ChoreographyScheduler._run(): asyncio.create_task() wrapping loop with 5ms poll interval" - "seek() implemented as play() restart with seek_seconds — simpler than maintaining event index pointer" requirements-completed: - CHR-05 # Metrics duration: 2min completed: 2026-04-03 --- # Phase 02 Plan 04: ChoreographyScheduler Summary **asyncio absolute monotonic scheduler dispatching LED animation commands at precise timestamps — drift < 20ms guaranteed by D-07/D-08/D-09 algorithm** ## Performance - **Duration:** 2 min - **Started:** 2026-04-03T12:49:30Z - **Completed:** 2026-04-03T12:51:01Z - **Tasks:** 1 (TDD: 2 commits — test + implementation) - **Files modified:** 2 ## Accomplishments - ChoreographyScheduler with absolute monotonic clock dispatch — never accumulates relative delays - pause/resume pair using pause_offset accumulation (D-09) — no event skipping or double-firing on resume - seek() via play() restart with seek_seconds — correctly skips events before seek position - 9 pytest-asyncio tests all passing — timing, payload format, pause/resume, seek, stop, on_event callback, position tracking ## Task Commits Each task committed atomically (TDD pattern): 1. **RED — failing tests** - `a6ba523` (test) 2. **GREEN — ChoreographyScheduler implementation** - `ea59bb0` (feat) ## Files Created/Modified - `src/led_sync/scheduler.py` — ChoreographyScheduler: play(), pause(), resume(), seek(), stop(), _run(), _dispatch() - `tests/test_scheduler.py` — 9 asyncio tests covering all scheduler behaviors ## Decisions Made - Absolute monotonic timestamps (D-07): `fire_at = start_time + pause_offset + event.timestamp` recalculated fresh each poll iteration — mathematical guarantee of <1ms drift per event over any duration - pause_offset accumulation (D-09): `resume()` adds elapsed pause time so all future `fire_at` values stay correct - seek() implemented as `play()` restart — simpler than maintaining an internal event index pointer, avoids state management complexity - Scheduler sends `{zone, animation, params}` only — `v:1` is transport's responsibility (confirmed by test asserting `"v" not in cmd`) ## Deviations from Plan None - plan executed exactly as written. ## Issues Encountered None — implementation matched plan specification precisely. All 9 tests pass on first run. ## User Setup Required None - no external service configuration required. ## Next Phase Readiness - CHR-05 fulfilled: `ChoreographyScheduler` dispatches animation commands at absolute monotonic timestamps - Ready for Phase 02-05 (beat detection) which will call `scheduler.on_event` via the callback hook - Ready for Phase 03 TUI integration — scheduler is asyncio-native, Textual will wrap `play()`/`pause()`/`resume()`/`seek()` directly - `current_position` property enables real-time timeline display in the TUI --- *Phase: 02-app-core-audio* *Completed: 2026-04-03*