How OpenLogi Handles HID++ Device Communication: Architecture and Implementation
OpenLogi handles HID++ device communication through a layered async architecture that abstracts raw HID transport into type-safe feature APIs using the hidpp protocol crate.
In the AprilNEA/OpenLogi repository, HID++ communication is implemented across four specialized crates that separate transport mechanics from high-level device control. This design enables robust async communication with Logitech peripherals while maintaining clean separation between protocol-level operations and application logic.
Architecture Overview
The codebase organizes HID++ communication into distinct layers, each with specific responsibilities and implementation files.
Transport Layer
The transport layer handles raw device detection and channel initialization. In crates/openlogi-hid/src/transport/native.rs, the system enumerates USB and BLE devices using enumerate_devices, filters for HID++ capability via is_hidpp_node, and establishes communication channels through open_hidpp_channel.
This layer creates a HidppChannel struct that wraps the raw HID device handle, providing the foundation for all higher-level operations.
Channel Layer
Located in crates/openlogi-hidpp/src/channel.rs, the channel layer implements the HidppChannel struct with async request-response semantics. Key capabilities include:
- Protocol adaptation: Automatically detects short vs. long report support via
supports_short_long_hidpp - Message sequencing: Adds sequence numbers to outgoing reports
- Error resilience: Implements transparent retry logic for transient failures
- Tracing integration: Instruments all HID++ transactions for debugging
The channel exposes a unified request method that accepts feature-specific request objects and returns decoded responses.
Feature API Layer
HID++ features are defined in crates/openlogi-hidpp/src/feature/*.rs as strongly-typed structs representing specific device capabilities like DPI control, backlighting, and SmartShift. The #[derive(Feature)] procedural macro in crates/openlogi-hidpp-derive/src/lib.rs generates the encoding and decoding logic for each feature, mapping Rust types to HID++ function IDs and payload layouts.
Device Layer
High-level device sessions reside in crates/openlogi-device/src/, where modules like write/dpi.rs and session/keyboard.rs orchestrate feature calls. These modules instantiate feature structs, populate payloads, and forward commands through the channel abstraction.
The Communication Flow
OpenLogi establishes HID++ communication through a five-step process:
-
Device Enumeration: The HID transport scans system devices and applies
is_hidpp_nodeto filter for Logitech HID++ capable peripherals based on usage page and ID criteria. -
Channel Initialization: For each valid device,
open_hidpp_channelconstructs aRawHidChanneland upgrades it to an asyncHidppChannel, querying the device to determine short or long report support. -
Request Construction: Application code creates feature-specific requests using generated structs (e.g.,
Dpi::set(800)), which implement theFeaturetrait for automatic serialization. -
Protocol Transmission: The channel's
requestmethod converts requests to HID++ reports, handles message length adaptation (short to long conversion when needed), writes to the device, and awaits the response. -
Response Handling: Incoming reports are deserialized into strongly-typed response structs, with errors triggering the transport layer's retry mechanisms as implemented in
crates/openlogi-hid/src/transport.rs.
Implementing HID++ Features
The feature system uses Rust's procedural macros to eliminate boilerplate. When you apply #[derive(Feature)] to a struct in crates/openlogi-hidpp/src/feature/, the macro expands to implement:
- Function ID mapping: Associates the struct with its HID++ feature index and function number
- Payload encoding: Converts Rust fields into HID++ report bytes according to the protocol specification
- Response parsing: Deserializes device replies into Rust types
For example, the DPI feature in crates/openlogi-hidpp/src/feature/dpi.rs uses this macro to generate methods like Dpi::get() and Dpi::set(dpi: u16) that handle the binary protocol details automatically.
Practical Examples
Enumerating and Connecting to HID++ Devices
use openlogi_hid::transport::Transport;
use openlogi_hid::transport::enumerate_hidpp;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Scan all HID devices and filter for HID++ capable ones
let hidpp_nodes = enumerate_hidpp().await?;
for node in hidpp_nodes {
println!("Found HID++ device: {:?}", node);
// Open an async HID++ channel for this node
let channel = node.open_hidpp().await?;
// Query the device's feature set
let features = channel.request(
openlogi_hidpp::feature::FeatureSet::get()
).await?;
println!("Supported features: {:?}", features);
}
Ok(())
}
Setting Device DPI via the Feature API
use openlogi_hidpp::feature::Dpi;
use openlogi_hidpp::channel::HidppChannel;
async fn set_device_dpi(
channel: &HidppChannel,
dpi: u16
) -> anyhow::Result<()> {
// Build a DPI-set request using the generated Feature implementation
let request = Dpi::set(dpi);
// Send the request and ignore the empty response
channel.request(request).await?;
Ok(())
}
Summary
- Transport abstraction:
crates/openlogi-hid/src/transport/native.rshandles device discovery and raw channel creation viaenumerate_hidppandopen_hidpp_channel. - Async protocol handling:
crates/openlogi-hidpp/src/channel.rsimplementsHidppChannelwith automatic short/long report detection and retry logic. - Type-safe features: The
#[derive(Feature)]macro incrates/openlogi-hidpp-derive/src/lib.rsgenerates protocol code for feature structs defined incrates/openlogi-hidpp/src/feature/*.rs. - High-level API:
crates/openlogi-device/src/write/dpi.rsand session modules demonstrate ergonomic device control built atop the feature layer.
Frequently Asked Questions
How does OpenLogi detect if a device supports HID++?
OpenLogi detects HID++ devices in crates/openlogi-hid/src/transport/native.rs using the is_hidpp_node function, which inspects the USB or BLE usage page and usage ID to identify Logitech HID++ compatible peripherals during enumeration.
What is the difference between short and long HID++ reports?
Short reports use a compact 7-byte format for simple commands, while long reports support 20-byte payloads for complex feature data. The HidppChannel in crates/openlogi-hidpp/src/channel.rs automatically queries the device via supports_short_long_hidpp and adapts messages accordingly, converting short to long format when necessary.
How does the Feature derive macro work?
The #[derive(Feature)] procedural macro defined in crates/openlogi-hidpp-derive/src/lib.rs parses feature struct definitions and generates implementations for the Feature trait, including methods to encode requests into HID++ report bytes and decode device responses back into Rust types.
Can OpenLogi handle multiple HID++ devices simultaneously?
Yes. The architecture supports concurrent device sessions through independent HidppChannel instances. Each device connection maintains its own async channel, and the transport layer uses tokio for non-blocking I/O across multiple peripherals.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →