// This skill should be used when writing, porting, or debugging device drivers in hypercolor-hal. Triggers on "add a driver", "port a driver", "implement protocol", "device not working", "wire format", "encode frame", "USB HID packet", "zerocopy struct", "CommandBuffer", "device database entry", "transport type", "frame encoding", "protocol implementation", "add device support", or any work in crates/hypercolor-hal/.
This skill should be used when writing, porting, or debugging device drivers in hypercolor-hal. Triggers on "add a driver", "port a driver", "implement protocol", "device not working", "wire format", "encode frame", "USB HID packet", "zerocopy struct", "CommandBuffer", "device database entry", "transport type", "frame encoding", "protocol implementation", "add device support", or any work in crates/hypercolor-hal/.
Hypercolor HAL Driver Development
Architecture Boundary
hypercolor-hal must never depend on hypercolor-core — that would create a circular dependency (core depends on hal). Key dependencies: hypercolor-types, nusb, zerocopy, hidapi, tokio, tokio-serial, midir, image, thiserror, tracing, and Linux-only async-hid, i2cdev.
Before USB Driver Surgery
For “device jank”, “USB jank”, or “all USB devices stutter” reports, query daemon telemetry before editing protocol code:
snapshot.render.latest_frame.gpu_sample_stale or output_frame_source=published_frame means LEDs may be receiving old sampled data before any USB write happens.
snapshot.usb.display_frames_delayed_for_led_total and wait times reveal shared USB actor display-lane contention, not necessarily LED protocol failure.
If all USB devices jank in unison and queues are healthy, investigate shared render sampling/output reuse first.
If output_frame_source=current_frame, gpu_sample_retry_hit=true, writes are fast, and slow-frame warnings are wake_late, look for host scheduler pressure such as active Rust/Servo builds before editing USB protocol code.
Queue frames_dropped is not automatically bad: capped devices intentionally replace stale pending payloads when render FPS exceeds device FPS. Compare fps_sent to fps_target and check write/queue latency plus errors.
If one family has high avg_write_ms, drops, or errors while others are clean, then inspect transport/protocol encoding.
Do not lower FPS, resolution, LED counts, or performance caps to hide driver symptoms.
Protocol Trait Contract
Every driver implements Protocol (in src/protocol.rs). Key methods:
name() → human-readable protocol name (&'static str)
init_sequence() → commands sent on device connect (mode switch, firmware probe)
encode_frame_into(&self, colors: &[[u8; 3]], commands: &mut Vec<ProtocolCommand>) — prefer this — reuses the command vector across frames (zero-alloc hot path)
encode_brightness(&self, brightness: u8) -> Option<Vec<ProtocolCommand>> — hardware brightness control
transfer_type tells the transport how to send — some devices mix HID feature reports for commands with bulk transfers for color data (Corsair LINK), or feature reports for commands with output reports for colors (Lian Li).
CommandBuffer API
CommandBuffer::new(commands) wraps a &mut Vec<ProtocolCommand> for zero-alloc frame encoding:
letmut buffer = CommandBuffer::new(commands);
buffer.push_struct(&my_packet, false, Duration::ZERO, COMMAND_DELAY, TransferType::HidReport);
// push_fill takes a FnOnce(&mut Vec<u8>) closure — write directly into the reusable buffer
buffer.push_fill(false, Duration::ZERO, Duration::ZERO, TransferType::Primary, |buf| {
buf.resize(65, 0x00);
});
// push_slice is a convenience wrapper over push_fill
buffer.push_slice(&raw_bytes, false, Duration::ZERO, Duration::ZERO, TransferType::Primary);
buffer.finish(); // truncates to actual used count
push_struct writes any IntoBytes + Immutable struct directly — no intermediate Vec<u8>.
push_fill signature: push_fill(expects_response, response_delay, post_delay, transfer_type, FnOnce(&mut Vec<u8>)).
Zerocopy Wire-Format Structs
Mandatory pattern for all fixed-size protocol packets: