daemon-development · git:20260820.4772c37 · 2026-08-20 · sha256 7eb67a749a543f59
daemon-development git:20260820.4772c37B
Immutable. This exact content never changes and is served at /api/v1/blob/7eb67a749a543f59.
---
name: daemon-development
description: >-
This skill should be used when working on the Hypercolor daemon, REST API,
WebSocket protocol, or render pipeline. Triggers on "daemon route", "API
endpoint", "WebSocket channel", "event bus", "render pipeline", "device
lifecycle", "AppState", "adaptive FPS", "HypercolorBus", "backend manager",
"daemon config", "MCP tool", "daemon startup", or any work in
crates/hypercolor-daemon/ or crates/hypercolor-core/src/engine/.
---
# Hypercolor Daemon Development
The daemon (`hypercolor-daemon`) serves the REST API, WebSocket protocol, and orchestrates the render pipeline. Runs on `127.0.0.1:9420`.
## AppState
`AppState` holds 35+ fields (many `Arc`-wrapped), shared with Axum handlers via `State<Arc<AppState>>` extractor. Key fields:
- `effect_engine: Arc<Mutex<EffectEngine>>` — active effect lifecycle
- `effect_registry: Arc<RwLock<EffectRegistry>>` — effect catalog (metadata, search, categories)
- `event_bus: Arc<HypercolorBus>` — system-wide event bus (broadcast + watch channels)
- `backend_manager: Arc<Mutex<BackendManager>>` — device output routing
- `config_manager: Option<Arc<ConfigManager>>` — wraps `ArcSwap<HypercolorConfig>` for lock-free reads; `None` in tests
- `device_registry: DeviceRegistry` — device tracking (internally `Arc`-wrapped, cloneable)
- `scene_manager: Arc<RwLock<SceneManager>>` — scene CRUD, priority stack, transitions
- `render_loop: Arc<RwLock<RenderLoop>>` — frame timing and pipeline skeleton
- `spatial_engine: Arc<RwLock<SpatialEngine>>` — maps canvas pixels to LED positions
- `library_store: Arc<dyn LibraryStore>` — favorites, presets, playlists (JSON-backed or in-memory)
- `credential_store: Arc<CredentialStore>` — AES-256-GCM encrypted network credentials
- `power_state: watch::Sender<OutputPowerState>` — global brightness and output state
**Why Mutex on EffectEngine/BackendManager?** They hold `dyn` trait objects that aren't `Sync`. `RwLock` requires `Sync` on the inner type.
## REST API Patterns
All routes under `/api/v1/`. Success envelope:
```rust
pub struct ApiResponse<T: Serialize> {
pub data: T,
pub meta: Meta,
}
pub struct Meta {
pub api_version: String, // "1.0"
pub request_id: String, // "req_{uuid_v7}"
pub timestamp: String, // ISO 8601 UTC, e.g. "2026-03-29T12:00:00.000Z"
}
```
Errors have exactly one rendering: the `IntoResponse` impl on `DomainError` in `src/domain/mod.rs`. A handler builds the variant that describes the failure and calls `.into_response()`; a helper returns `Result<T, DomainError>` so the route can `?` it or match on it. Constructors read like the failure: `DomainError::not_found(ResourceKind::Scene, id)`, `validation(msg)`, `validation_field(field, msg)`, `malformed(msg)`, `conflict(msg)`, `unauthorized(msg)`, `forbidden(msg)`, `unsupported_media_type(msg)`. Never hand-build an error body and never hand a `Response` back through a `Result`; `tests/api_error_surface_tests.rs` scans every file under `src/` for both and fails the build.
The error envelope is `{ error: { code, message, details }, meta }`, where `details` is omitted entirely when the variant carries no structured context. See `references/api-patterns.md` for the full variant, code, and status table.
Key route groups (path parameters use `{id}` Axum syntax, not `:id`):
| Prefix | Purpose |
| --------------------------- | ------------------------------------------------------- |
| `/effects` | Effect catalog, detail, apply, install, rescan |
| `/effects/{id}/apply` | Replace one live zone stack with an effect |
| `/scene` | Read or patch the complete live scene document |
| `/scene/zones` | Live zone, member, layout, layer, and control mutations |
| `/devices` | Connected devices, discovery, pairing, segments |
| `/devices/{id}/attachments` | Embedded attachment bindings and validation |
| `/attachments/templates` | Attachment template collection |
| `/scenes` | Scene CRUD + snapshot + `{id}/activate` |
| `/library/favorites` | Favorites CRUD |
| `/library/presets` | User preset CRUD |
| `/library/playlists` | Playlist CRUD + activate/deactivate |
| `/layouts` | Spatial layout CRUD + active + preview + `{id}/apply` |
| `/config` | Show/get/set/reset system config values |
| `/output` | Global output power and brightness get/patch |
| `/system` | Public identity plus authorized daemon status |
| `/diagnose` | System diagnostics |
## WebSocket Protocol
Single endpoint at `/api/v1/ws`. Five of the fifteen topics:
| Topic | Data | Format |
| ---------- | ---------------------------------------------------- | ---------------------- |
| `events` | State changes (effect applied, device connected) | JSON |
| `frames` | LED color output per device | Binary |
| `canvas` | Render canvas pixels (default 640x480, configurable) | Binary (header `0x03`) |
| `spectrum` | Audio analysis (FFT, beats) | JSON |
| `metrics` | Performance telemetry (FPS, frame times) | JSON |
**Subscribe on connect:**
```json
{
"type": "subscribe",
"topics": [{ "topic": "events" }, { "topic": "metrics" }]
}
```
**Backpressure**: Slow consumers get dropped frames, not memory growth. The WS handler sends a `Backpressure` server message (JSON) with `dropped_frames`, `topic`, `recommendation: "reduce_fps"`, and `suggested_fps` so the UI can auto-throttle.
**Keyed topics**: `display_preview` (keyed by device) and `interactive_preview` (keyed by the client's preview id) hold one subscription per key, so a selector names both the topic and its key. Subscribing to `interactive_preview` is what opens its render lane.
## Event Bus (HypercolorBus)
Two communication patterns on the bus, all lock-free:
| Pattern | Channel | Use |
| -------------------------- | ------------------------ | -------------------------------------------------------- |
| `broadcast` (256 capacity) | `tokio::sync::broadcast` | Discrete state changes (`HypercolorEvent` variants) |
| `watch` (latest-value) | `tokio::sync::watch` | Frame data, spectrum, canvas (consumers see latest only) |
Events are wrapped in `TimestampedEvent` with ISO 8601 `timestamp` and `mono_ms` (monotonic millis since bus creation) for frame correlation. The bus is `Send + Sync` and shared via `Arc<HypercolorBus>`.
## Render Pipeline (5 Stages)
Runs on a **dedicated OS thread** with its own Tokio runtime (isolated from API thread pool):
| Stage | Budget | What Happens |
| -------------- | ------ | ---------------------------------------------- |
| Input Sampling | 1.0ms | Audio DSP + screen capture |
| Effect Render | 8.0ms | `EffectEngine::tick()` → Canvas |
| Spatial Sample | 0.5ms | Canvas pixels → LED colors via zone positions |
| Device Push | 2.0ms | Route colors to `BackendManager` → USB/network |
| Bus Publish | 0.1ms | Broadcast state events + watch updates |
**Adaptive FPS**: Tiers at 10/20/30/45/60. On 2 consecutive budget misses → downshift. On sustained headroom → upshift. Prevents frame drops from cascading.
## Modern Diagnostics & Telemetry
Do not ask Bliss to paste logs first. The dev environment should query the daemon:
```bash
just diagnose -- --json
hypercolor diagnose --system -j
curl -s -X POST http://127.0.0.1:9420/api/v1/diagnose -H 'content-type: application/json' -d '{"system":true}'
curl -s http://127.0.0.1:9420/api/v1/system
```
`/api/v1/diagnose` returns checks plus `snapshot.render`, `snapshot.usb`, and `snapshot.device_output`. The WebSocket `metrics` channel and authenticated `/api/v1/system` status expose the same latest-frame fields for live UI/agent inspection.
Key LED-frame fields:
- `output_frame_source`: `current_frame`, `published_frame`, or `routed_reuse`
- `output_reuses_published_frame`: render reused the last published LED frame data
- `output_routing_signature`, `output_zone_shape_signature`, `output_unassigned_behavior_generation`: changed signatures explain why output reuse did or did not happen
- `devices_written`, `total_leds`, `sample_us`, `push_us`, `publish_us`, `total_us`
- GPU sampling flags: `gpu_sample_deferred`, `gpu_sample_stale`, `gpu_sample_retry_hit`, `gpu_sample_queue_saturated`, `gpu_sample_wait_blocked`, `gpu_sample_cpu_fallback`, `cpu_readback_skipped`, `gpu_readback_failed`
Key device-output fields:
- Per queue: `backend_id`, `mapped_layout_ids`, `uses_frame_sink`, `worker_finished`, `delivered_fps`, `accepted_fps`, `coalesced_target_cadence`, `coalesced_backend_overrun`, `transport_started`, `transport_completed`, `transport_failed`, `avg_queue_wait_ms`, `avg_transport_latency_ms`, `queue_generation`, and delivery sequence watermarks
- USB actor: `display_frames_delayed_for_led_total`, `display_led_priority_wait_avg_ms`, `display_led_priority_wait_max_ms`
Jank triage:
- Displays smooth but LEDs jank: inspect `output_frame_source` and GPU stale/deferred flags before touching USB.
- All USB LEDs jank in unison: suspect shared LED sample/output freshness or shared queue pressure before per-device protocol bugs.
- One device/family janks: compare `delivered_fps` with the target, split target-cadence coalescing from backend-overrun coalescing, then inspect queue wait, transport latency, and `last_error`.
- `output_frame_source=current_frame`, high `gpu_sample_retry_hit`, low sample/push times, and `wake_late` warnings point at host scheduler pressure rather than the LED pipeline. On Windows, check for active `cargo.exe`/`rustc.exe`/`link.exe`/`cl.exe` jobs before tuning render code; direct repo-local Cargo builds can steal enough CPU to wake the render thread several milliseconds late.
- `coalesced_target_cadence` is expected latest-frame replacement when a device has a lower target FPS than the render loop. Treat `coalesced_backend_overrun` as pressure: correlate it with queue wait, transport latency, failures, and delivered cadence.
- Never reduce canvas resolution, FPS ceilings, or performance caps to hide telemetry symptoms. Fix the root cause.
## Device Lifecycle State Machine
Per-device states managed by `DeviceLifecycleManager` — a **pure state machine** that emits actions for async executors (no I/O itself):
```mermaid
stateDiagram-v2
Unknown --> Known
Known --> Connecting
Connecting --> Connected
Connected --> Active
Connecting --> Failed
Connected --> Disconnected
Disconnected --> Failed
Failed --> Connecting : exponential backoff\n6 max attempts, 30s max delay
```
Hot-plug: USB device events trigger state transitions. The lifecycle manager decides whether to reconnect, the executor performs the actual transport operations.
## Configuration
- **Config file**: TOML (`config.toml`), loaded by `ConfigManager` which wraps `ArcSwap<HypercolorConfig>` for lock-free reads
- **Data storage**: `~/.local/share/hypercolor/` (8 JSON data files: `scenes.json`, `layouts.json`, `library.json`, `device-settings.json`, `attachment-profiles.json`, `layout-auto-exclusions.json`, `logical-devices.json`, `runtime-state.json`)
- **Hot-reload**: `ConfigManager` uses `Arc<ArcSwap<HypercolorConfig>>` for atomic pointer swap on config change
- **Encrypted**: Credentials stored via `CredentialStore` using AES-256-GCM encryption (file-backed, not keyring)
## MCP Server Integration
17 tools exposed via Model Context Protocol for AI control:
| Tool | Purpose |
| ------------------ | ------------------------------------------------------------------ |
| `set_effect` | Apply one deterministically selected effect with optional controls |
| `list_effects` | Browse effect catalog with category/audio_reactive filters |
| `set_color` | Apply a solid color effect |
| `set_output_power` | Pause or resume output without discarding effect state |
| `clear_zone` | Clear one non-display zone or every non-display zone |
| `adjust_controls` | Patch typed values and clear bindings on one live layer |
| `get_devices` | List connected devices |
| `set_brightness` | Set global brightness (0-100 percent) |
| `get_status` | Current daemon state snapshot |
| `activate_scene` | Activate a scene by name/ID |
| `list_scenes` | List all scenes |
| `create_scene` | Create a new scene |
| `get_audio_state` | Audio analysis snapshot |
| `get_sensor_data` | System telemetry snapshot or one named sensor reading |
| `set_display_face` | Assign an HTML display face to a display device |
| `get_layout` | Get the active spatial layout |
| `diagnose` | Canonical safe diagnostics from the shared REST collector |
Every tool declares `read_only`, `destructive`, and `idempotent` annotations.
The destructive set is `set_effect`, `set_color`, `clear_zone`,
`activate_scene`, and `set_display_face`. `adjust_controls` is a
non-destructive atomic patch.
`diagnose` accepts only an empty closed object and returns the canonical REST
diagnostic data object: `checks`, `summary`, and `snapshot`. It always runs the
safe default checks and never exposes the protected `macos_screen_parity` check.
5 resources: `hypercolor://state`, `hypercolor://devices`, `hypercolor://effects`, `hypercolor://scenes`, `hypercolor://audio`. Empty `get_status` and `get_devices` calls use the same builders as their matching resources. Effect, scene, zone, layer, device, and display-face selectors resolve exact IDs, exact case-insensitive names, or unique case-insensitive name substrings; ambiguity returns structured candidates. Color text uses its separate fuzzy resolver.
## Detailed References
- **`references/api-patterns.md`** — Full route catalog with request/response shapes, middleware chain, error handling conventions
- **`references/event-bus.md`** — Event taxonomy, frame correlation with mono_ms, subscription patterns, backpressure tuning