Debugging & diagnostics
RUST_LOG targets, trace spans, diagnose command, Servo troubleshooting, and display-face simulator debugging.
When things go wrong β devices not responding, effects rendering incorrectly, audio not reacting, Servo hanging β here is how to find and fix the problem.
πDaemon logs
The daemon uses tracing with a themed console formatter. Control verbosity with RUST_LOG or the --log-level flag. The default is info, which logs device connections, effect changes, and errors without flooding the terminal.
# Debug logging (recommended starting point)
RUST_LOG=debug just daemon
# Maximum verbosity: every frame, every USB packet
RUST_LOG=trace just daemon
# Specific module, rest at info
RUST_LOG=hypercolor_hal=debug,hypercolor_core::engine=trace just daemon
# Warnings and errors only
RUST_LOG=warn just daemonThe app shell (hypercolor-app) writes a separate rolling log file to the data directory. Its default filter is hypercolor_app=debug,tauri=info,wry=warn. Override it the same way with RUST_LOG.
πKey log targets
| Target | What it shows |
|---|---|
hypercolor_daemon | API requests, WebSocket connections, startup sequence |
hypercolor_core::engine | Render loop timing, FPS tier shifts, frame drops |
hypercolor_core::effect | Effect loading, control updates, renderer state |
hypercolor_core::effect::servo | Servo worker lifecycle, session create/destroy, circuit breaker |
hypercolor_core::input | Audio capture, FFT processing, beat detection |
hypercolor_hal | Device discovery, USB communication, protocol encoding |
hypercolor_hal::drivers::razer | Razer-specific USB packet detail |
hypercolor_hal::drivers::prismrgb | PrismRGB chunked protocol detail |
πBuilt-in diagnostics
hypercolor diagnose posts to POST /api/v1/diagnose and runs a set of named health checks against the live daemon. The default check set is daemon, render, devices, config.
# Tabular output (the default, easiest to read)
hypercolor diagnose
# Run specific checks only (repeatable)
hypercolor diagnose --check render --check devices
# Add verbose system information (GPU, kernel, audio version, uptime)
hypercolor diagnose --system
# Write a full report file for bug reports
hypercolor diagnose --report /tmp/hypercolor-report.json
# Raw JSON via REST
curl -s -X POST http://localhost:9420/api/v1/diagnose | jqEach check produces a pass, warning, or fail status with a detail string. The summary line at the end counts totals across all checks.
πDiagnose checks reference
| Check | Category | What it tests |
|---|---|---|
daemon | system | Daemon is running and returns its version |
render | render | Render loop state, frame liveness (stale > 2 s β warning, > 10 s β fail), LED freshness |
devices | devices | Registry count, output queue health, USB actor display lane, display output encoder |
config | config | Config manager availability |
The response also includes a snapshot object with detailed render timing, USB actor metrics, display output encoder counters, and per-device queue state. Inspect it with:
curl -s -X POST http://localhost:9420/api/v1/diagnose | jq '.data.snapshot'The per-device items under snapshot.device_output.items include accepted and delivered FPS, target-cadence versus backend-overrun coalescing, actual transport latency, terminal counters, and last_error. That split distinguishes healthy latest-wins pacing from a lagging device.
πServo memory diagnostics
On non-Windows builds with the servo feature enabled, a separate endpoint captures Servoβs internal memory profiler output:
curl -s -X POST http://localhost:9420/api/v1/diagnose/memory | jqThis returns a ServoMemoryReportSnapshot with per-process explicit heap, system heap, non-heap, and non-explicit bytes. Useful for tracking down long-session Servo memory growth.
Servo memory diagnostics are disabled on Windows because the embedded memory reporter can abort the daemon process. The endpoint returns 404 on that platform. On Linux/macOS builds without the servo feature the endpoint also returns 404.
πREST and WebSocket inspection
# Full system status (includes audio_available and effect_health)
curl -s http://localhost:9420/api/v1/status | jq
# Connected devices and their current state
curl -s http://localhost:9420/api/v1/devices | jq
# Active effect
curl -s http://localhost:9420/api/v1/effects/active | jq
# Real-time event stream (install websocat if needed: cargo install websocat)
websocat ws://localhost:9420/api/v1/ws | jqThe WebSocket stream shows effect changes, device events, control updates, and frame metrics as they happen. It is the fastest way to understand timing and sequencing. See the WebSocket reference for channel details and binary frame layout.
πUSB debugging
πPermission issues
If devices are discovered but fail to connect, it is almost always a permissions problem:
# Check if udev rules are installed
ls -la /etc/udev/rules.d/99-hypercolor.rules
# Install them (requires sudo)
just udev-install
# Verify device file permissions
ls -la /dev/hidraw*
# See recent kernel messages for HID/USB events
sudo dmesg | grep -i "hid\|usb" | tail -20The rules grant access to the physically logged-in user via systemd-logind (TAG+="uaccess"), with GROUP="users" as a fallback. After installing them, re-plug the device or reboot so existing device nodes pick up the new ACLs.
πDevice not discovered
# Check if the device is visible to the system
lsusb
# Get detailed USB descriptor info for a specific device
lsusb -v -d VENDOR:PRODUCT 2>/dev/null
# List HID device files
ls /dev/hidraw*
# Watch udev events in real time, then plug in the device
sudo udevadm monitor --propertyIf lsusb shows the device but it does not appear in Hypercolor, run the daemon at debug level and look for discovery output from hypercolor_hal. The vendor/product ID may not be in the device database yet.
πAudio debugging
πEffects not reacting to audio
Check that audio capture is working at the system level:
# List PulseAudio/PipeWire sources pactl list sources short # Confirm a monitor source exists (what you hear through speakers) pactl list sources short | grep monitorCheck the daemon audio state:
curl -s http://localhost:9420/api/v1/status | jq '.data.audio_available'Enable audio input tracing to see what the daemon receives:
RUST_LOG=hypercolor_core::input=debug just daemon
For audio-reactive effects, you want a βmonitorβ source that captures your system audio output. On PulseAudio and PipeWire these are named like alsa_output.*.monitor.
πEffect debugging
πEffect not loading
# Check daemon logs for loading errors
RUST_LOG=hypercolor_core::effect=debug just daemon
# List loaded effects after startup
curl -s http://localhost:9420/api/v1/effects | jq '.data.items[].name'πEffect rendering issues
Run with effect engine and render loop tracing:
RUST_LOG=hypercolor_core::effect=trace,hypercolor_core::engine=debug just daemonThis surfaces which renderer is active, frame timing, control value injection, and audio delivery to the effect.
πServo troubleshooting
HTML effects β TypeScript SDK canvas effects, GLSL shaders (via WebGL2), raw HTML, and display faces β all render through the Servo worker. Servo has its own failure modes.
πStart the Servo-enabled daemon
The standard just daemon command does not include Servo. To test HTML effects you need:
just daemon-servoπServo-specific log targets
RUST_LOG=hypercolor_core::effect::servo=debug just daemon-servoKey sub-targets:
| Target | What it shows |
|---|---|
hypercolor_core::effect::servo | Session lifecycle, page loads, render requests, circuit breaker |
hypercolor_core::effect::servo::worker | Worker thread spawn/teardown, command channel health |
hypercolor_core::effect::servo::renderer | EffectRenderer facade, render submission timing |
πCircuit breaker
The Servo worker has a circuit breaker that opens after 3 consecutive soft failures and applies exponential backoff starting at 30 seconds, capped at 5 minutes. If effects suddenly stop rendering after a crash or hang, the breaker may be open.
Signs the breaker has opened:
- Effect applies without error but LEDs stop updating.
hypercolor_core::effect::servologs show repeated worker acquisition failures.servo_breaker_opens_totalincrements in theeffect_healthfield of the status endpoint.
The breaker resets automatically after the cooldown. Restarting the daemon also clears it. Servo telemetry lives under effect_health on the status endpoint β not in the diagnose snapshot:
curl -s http://localhost:9420/api/v1/status | jq '.data.effect_health'πServo session failures
The Servo worker manages sessions in an Idle β Loading β Running state machine. Session creation and page load failures are counted under effect_health:
curl -s http://localhost:9420/api/v1/status | jq '
.data.effect_health |
{
session_creates: .servo_session_creates_total,
session_create_failures: .servo_session_create_failures_total,
page_loads: .servo_page_loads_total,
page_load_failures: .servo_page_load_failures_total,
breaker_opens: .servo_breaker_opens_total
}
'For full Servo telemetry (render queue waits, GPU import stats, per-frame timing), inspect every servo_* field under effect_health:
curl -s http://localhost:9420/api/v1/status | jq '.data.effect_health | with_entries(select(.key | startswith("servo_")))'πServo CSS and layout constraints
CSS grid does not work in Servo: children render stacked full-width instead of in a grid. Use flexbox for display-face layouts. This is a known Servo limitation, not a Hypercolor bug.
WebGL2 works via the Servo canvas but not all extensions available in Chrome are present. If a GLSL effect works in a browser but not in Hypercolor, check for extension dependencies.
There is no native GPU/wgpu shader lane. EffectSource::Shader is reserved for future work and is not currently executed. GLSL effects run as WebGL2 inside Servo. Do not expect a native compiled shader path to be available.
πServo OOM or crash
If the daemon process exits or becomes unresponsive while running HTML effects:
Run
just daemon-servowithRUST_LOG=hypercolor_core::effect::servo=traceto capture the last log lines before exit.Check Servo memory before and after loading the problematic effect:
curl -s -X POST http://localhost:9420/api/v1/diagnose/memory | jq '.data.totals'If explicit heap grows unboundedly across effect restarts, the effect may be holding DOM references across render frames. Move all allocations inside the draw callback.
πDisplay-face simulator debugging
Display faces render to virtual LCD displays. When debugging face effects, use the virtual simulator instead of requiring physical hardware.
πCreate a simulator via REST
# Create a rectangular simulator (e.g., Corsair LCD module size)
curl -s -X POST http://localhost:9420/api/v1/simulators/displays \
-H 'Content-Type: application/json' \
-d '{"name":"test-lcd","width":480,"height":270,"circular":false}' | jq
# Create a circular simulator (e.g., AIO cooler LCD)
curl -s -X POST http://localhost:9420/api/v1/simulators/displays \
-H 'Content-Type: application/json' \
-d '{"name":"round-lcd","width":240,"height":240,"circular":true}' | jqThe SDKβs just face-dev NAME command builds and installs the face, creates a round and a strip simulator display, assigns the face to both, and rebuilds on save β it is the recommended development loop for display faces.
πInspect simulator output
The daemon serves a JPEG frame per simulated display. List active simulators first:
curl -s http://localhost:9420/api/v1/simulators/displays | jqThen fetch the current frame:
curl -s http://localhost:9420/api/v1/simulators/displays/{id}/frame -o /tmp/face-preview.jpgThe display preview WebSocket channel (binary tag 0x07) streams JPEG frames to subscribers. Those payloads are binary JPEG, not JSON, so you subscribe with a config message rather than piping through jq. See the WebSocket reference for the display_preview channel configuration and frame layout.
πFace dev log targets
RUST_LOG=hypercolor_core::effect::servo=debug,hypercolor_daemon::render_thread::display_lane=debug just daemon-servoThe ServoProducerRole::DisplayFaceHtml variant is tracked separately from scene HTML rendering in telemetry. Look for servo_render_display_requests_total versus servo_render_scene_requests_total under effect_health on the status endpoint to confirm the face is actually submitting render requests.
πTwo-geometry face dev
just face-dev NAME spins up two simulators at once β a 480x480 round display and a 960x160 strip β so you can watch a face render on both a circular and a rectangular surface side by side. Validate the built artifact before installing with bun run validate, which checks metadata and render surfaces.
If a face looks wrong on the round display:
- Check
ctx.display.circularin your draw function. Round displays clip to a circle, so honor thesafeAreainset (339x339 centered on a 480x480 round LCD) for content that must stay visible. - Use
clip-path: circle()or the SDKβs container mask rather than CSS grid, which Servo does not support.
πCommon issues quick reference
βPermission deniedβ on USB devices β install udev rules with just udev-install, then re-plug the device or reboot so existing nodes pick up the new ACLs. The rules use uaccess for the logged-in user with GROUP="users" as a fallback.
Daemon starts but no devices connect β run lsusb to confirm the device is visible. If visible, the VID/PID may not be in the device database. Run at debug level and look for discovery output from hypercolor_hal.
Effects apply but LEDs show wrong colors β likely a spatial layout mismatch. Verify device zones are positioned correctly. Use hypercolor devices identify <id> to confirm the device is receiving data.
Low FPS in the render loop β enable RUST_LOG=hypercolor_core::engine=trace and look for frame time spikes. Common causes: slow USB writes (check avg_write_ms per device in the diagnose snapshot), Servo rendering overhead (check servo_render_frame_max_ms under effect_health on the status endpoint), or too many devices on one USB controller.
WebSocket connection drops β the daemon pings clients periodically and drops non-responding ones. Verify your client handles WebSocket ping/pong. Also check for reverse proxy or firewall idle-connection timeouts.
Servo circuit breaker open, effects frozen β wait for the cooldown to expire (30 s minimum, up to 5 min with repeated failures) or restart the daemon. If it reopens immediately, set RUST_LOG=hypercolor_core::effect::servo=trace to capture the failure reason before the breaker trips again.
Servo memory diagnostics return 404 β this endpoint is disabled on Windows. On Linux/macOS with the servo feature it should always be available; without the feature it also returns 404.