How OpenLogi Implements the HID++ Protocol: From Device Registry to the HID Layer
OpenLogi's HID++ implementation uses a three-layer zero-dependency architecture separating hardware identity (device registry), transport enumeration (HID layer), and protocol commands (HID++ channel with typed feature APIs).
The OpenLogi project provides a fully open-source, Rust-native stack for controlling Logitech HID++ devices without proprietary software. Understanding how the HID++ protocol implementation flows from static device definitions through to high-level feature calls requires tracing three distinct architectural layers. This article walks through the complete data path: from compile-time hardware constants in openlogi-device-registry, through OS-level HID enumeration in openlogi-hid, to the typed protocol channel in openlogi-hidpp.
What Is the HID++ Protocol?
HID++ is Logitech's proprietary extension to the USB HID (Human Interface Device) standard. It enables advanced device features—adjustable DPI, RGB backlighting, smart shift, battery reporting—beyond standard mouse and keyboard inputs. The protocol operates over HID report structures, using either short reports (7 bytes) or long reports (20 bytes) depending on device capabilities and connection type.
OpenLogi's implementation adds async-first Rust APIs with compile-time feature detection, eliminating runtime dependencies on Logitech's proprietary drivers or daemons.
Layer 1: Device Registry – Static Hardware Definitions
The openlogi-device-registry crate provides pure, #![no_std] constants that identify Logitech hardware families. This layer answers "what devices exist?" without any runtime I/O.
Key definitions in crates/openlogi-device-registry/src/lib.rs:
pub const LOGITECH_VENDOR_ID: u16 = 0x046d;
The registry organizes hardware into sub-modules:
litra– Product IDs for Litra lighting devicesreceiver– USB receiver families (Unifying, Bolt, etc.)
These constants enable compile-time matching against enumerated HID devices. The registry contains no logic—only structs and const values—making it suitable for embedded targets and deterministic builds.
Source: [crates/openlogi-device-registry/src/lib.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-device-registry/src/lib.rs)
Layer 2: HID Transport – Enumerating and Opening HID++ Nodes
The openlogi-hid crate bridges the OS HID stack to OpenLogi's protocol layer. It transforms raw OS device handles into typed HidppChannel instances ready for HID++ communication.
Detecting HID++ Capable Devices
The transport layer filters HID devices using usage page/ID matching. The is_hidpp_node helper in crates/openlogi-hid/src/transport/native.rs identifies devices speaking HID++:
pub(crate) fn is_hidpp_node(device: &async_hid::Device) -> bool {
// Checks usage page/id against known HID++ collections
// USB receivers, BLE-direct connections, wired G-series peripherals
}
This function distinguishes HID++ devices from standard HID keyboards and mice sharing the same USB vendor ID.
Opening a Typed HID++ Channel
Once identified, open_hidpp_channel wraps the raw device in protocol-aware structures:
async fn open_hidpp(&self, node: &NodeInfo) -> Result<Option<Arc<HidppChannel>>, BackendError> {
open_hidpp_channel(&device, self.device_io.clone()).await
}
The resulting Arc<HidppChannel> provides reference-counted access to the device across async tasks. The channel automatically detects whether the underlying device supports short reports, long reports, or both—information cached for subsequent operations.
Source: [crates/openlogi-hid/src/transport/native.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hid/src/transport/native.rs)
Short vs. Long Report Detection
The RawHidChannel::supports_short_long_hidpp method queries device capabilities during initialization. This determines whether the protocol layer must up-convert short messages to long format for devices that only support 20-byte reports.
Source: [crates/openlogi-hidpp/src/channel/raw.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hidpp/src/channel/raw.rs)
Layer 3: HID++ Protocol – Typed Feature Access
The openlogi-hidpp crate implements the complete protocol specification: message framing, request/response correlation, feature discovery, and high-level device capabilities.
Core Components
| Component | Responsibility | Key File |
|---|---|---|
RawHidChannel |
Raw HID report I/O, capability detection | src/channel/raw.rs |
HidppChannel |
Async request/response, error handling, format conversion | src/channel.rs |
Message |
Wire-format encoding/decoding of HID++ structs | src/channel/message.rs |
| Feature modules | Typed APIs per capability (DPI, backlight, etc.) | src/feature/*.rs |
The HidppChannel Request Flow
HidppChannel implements the async-trait interface consumed by higher layers:
- Receives a
Messagefrom a feature implementation - Determines correct report length (short or long)
- Writes raw bytes via
RawHidChannel::write_report - Awaits response with timeout and retry logic
- Parses response bytes back into a
Message
For devices supporting only long reports, the channel automatically pads short messages—protocol transparency the caller need not handle.
Feature Modules: Typed Device Capabilities
Each HID++ capability lives in a dedicated module under crates/openlogi-hidpp/src/feature/. The openlogi_hidpp_derive::Feature macro generates boilerplate for:
- Feature discovery (querying which protocol version a device supports)
- Request/response struct definitions
- Async getter/setter methods
Example: DPI Feature
use openlogi_hidpp::feature::dpi::DpiFeature;
use openlogi_hidpp::channel::HidppChannel;
// `channel` is Arc<HidppChannel> from the transport layer
let dpi = DpiFeature::get_dpi(&channel).await?;
println!("Current DPI: {}", dpi);
The DpiFeature::get_dpi method:
- Constructs a
Message::ReadDpirequest - Calls
HidppChannel::requestfor transport - Extracts the DPI value from the response payload
Source: DpiFeature implementation in [crates/openlogi-hidpp/src/feature/dpi.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hidpp/src/feature/dpi.rs); usage in [crates/openlogi-device/src/write/dpi.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-device/src/write/dpi.rs)
Complete Example: Enumerating and Controlling HID++ Devices
This runnable example demonstrates the full stack from transport enumeration to feature control:
use openlogi_hid::transport::NativeHidBackend;
use openlogi_hidpp::feature::dpi::DpiFeature;
use futures::stream::StreamExt;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Initialize platform-specific HID backend
let backend = NativeHidBackend::new()?;
// Enumerate all HID++ capable devices
let mut devices = backend.enumerate_hidpp().await?;
while let Some(node) = devices.next().await {
println!("Found HID++ device: {}", node.product_name);
// Open protocol channel for this device
if let Some(channel) = backend.open_hidpp(&node).await? {
// Query DPI via typed feature API
match DpiFeature::get_dpi(&channel).await {
Ok(dpi) => println!(" Current DPI: {}", dpi),
Err(e) => println!(" DPI query failed: {}", e),
}
}
}
Ok(())
}
Key APIs referenced:
NativeHidBackend– platform abstraction inopenlogi-hid/src/transport/native.rsenumerate_hidpp()– returns stream ofNodeInfostructsopen_hidpp()– returnsOption<Arc<HidppChannel>>DpiFeature::get_dpi()– typed protocol call inopenlogi-hidpp/src/feature/dpi.rs
Windows-Specific Transport Implementation
OpenLogi provides platform-specific optimizations. The Windows transport in crates/openlogi-hid/src/transport/windows.rs uses native Win32 HID APIs when cfg(windows) is active, falling back to the generic async-hid path for cross-platform builds.
Source: [crates/openlogi-hid/src/transport/windows.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hid/src/transport/windows.rs)
Message Format and Wire Encoding
HID++ messages use nibble-packed fields for compact encoding. The Message struct in crates/openlogi-hidpp/src/channel/message.rs provides:
- Device index – targets specific devices on multi-device receivers
- Feature index – identifies which capability is being addressed
- Function ID – operation within that feature (get/set/event)
// Simplified message structure
pub struct Message {
pub device_index: u8,
pub feature_index: u8,
pub function_id: u8,
pub params: [u8; 16], // Parameter payload
}
The encoding handles protocol version differences transparently.
Source: [crates/openlogi-hidpp/src/channel/message.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hidpp/src/channel/message.rs)
Architecture Benefits
This three-layer separation provides concrete engineering advantages:
- Testability – Protocol logic in
openlogi-hidppruns against mockRawHidChannelimplementations without hardware - Portability – New OS transports only implement
HidBackend; feature code is unchanged - Extensibility – Adding new device capabilities requires only a new feature module and derive macro
- Safety – Compile-time zero-cost abstractions prevent protocol errors (wrong message length, invalid feature index)
Summary
- Device Registry (
openlogi-device-registry) – Static vendor/product constants in#![no_std]Rust - HID Transport (
openlogi-hid) – OS enumeration, HID++ node detection viais_hidpp_node, channel opening - HID++ Protocol (
openlogi-hidpp) –RawHidChannelfor raw I/O,HidppChannelfor async request/response, feature modules for typed APIs - Report Format Handling – Automatic short-to-long conversion based on
supports_short_long_hidppdetection - Entry Points –
NativeHidBackend::enumerate_hidpp()andopen_hidpp()for application developers
Frequently Asked Questions
What is the difference between RawHidChannel and HidppChannel?
RawHidChannel in crates/openlogi-hidpp/src/channel/raw.rs performs basic HID report read/write operations and detects short/long report support. HidppChannel in crates/openlogi-hidpp/src/channel.rs wraps this with HID++-specific logic: request/response correlation, retry handling, message framing, and automatic format conversion. Application code uses HidppChannel; RawHidChannel is an internal abstraction.
How does OpenLogi detect whether a USB device uses HID++?
The transport layer calls is_hidpp_node in crates/openlogi-hid/src/transport/native.rs, which inspects the HID usage page and usage ID of each enumerated device. Logitech receivers and HID++ peripherals expose specific collection types that distinguish them from standard HID mice and keyboards, even when sharing the same USB vendor ID (0x046d).
Can OpenLogi control HID++ devices on platforms without native Logitech drivers?
Yes. OpenLogi's zero-dependency design operates entirely in userspace through standard OS HID APIs. No proprietary drivers, kernel modules, or background daemons are required. The project uses async-hid for cross-platform HID access and provides native Windows transport as an optimization.
How are new HID++ features like RGB lighting or smart shift added?
Developers create a new file in crates/openlogi-hidpp/src/feature/ using the openlogi_hidpp_derive::Feature procedural macro. This generates discovery, request construction, and response parsing code. The feature module defines getter/setter methods that operate on HidppChannel, which the device layer (crates/openlogi-device/src/write/) calls to implement user-facing operations.
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 →