HID++ Protocol Implementation in OpenLogi for Logitech Devices
OpenLogi implements Logitech's proprietary HID++ protocol through the openlogi-hidpp crate, providing pure-Rust async communication with mice, keyboards, and receivers via message framing, software-ID management, and dynamic feature discovery.
The HID++ protocol implementation in OpenLogi enables direct control of modern Logitech peripherals without relying on proprietary SDKs or binary blobs. Located in the crates/openlogi-hidpp directory of the AprilNEA/OpenLogi repository, this Rust implementation abstracts the complexities of HID++ 1.0 and 2.0 wire formats while exposing device capabilities through a type-safe feature registry.
Core Architecture and Components
The implementation follows a layered architecture that separates raw HID transport from high-level protocol logic.
Raw HID Bridge (RawHidChannel)
At the lowest level, the RawHidChannel trait defined in crates/openlogi-hid/src/transport/native.rs provides a hardware-agnostic interface. This abstraction allows OpenLogi to work with different HID back-ends—such as async-hid or platform-native APIs—without modifying the protocol layer. Any HID library can implement this trait to become a transport provider for the HID++ stack.
HID++ Channel (HidppChannel)
The HidppChannel struct in crates/openlogi-hidpp/src/channel.rs represents the core protocol engine. It handles:
- Message framing for short (8-byte) and long (20-byte) HID++ reports
- Request-response matching using a
pending_messagesqueue - Asynchronous I/O with background read loops
- Software-ID policies via the
SwIdPolicyenum to manage 4-bit transaction identifiers
When initialized via HidppChannel::from_raw_channel, the channel probes the device to determine HID++ version support and automatically configures the appropriate message format.
Device Abstraction (Device)
The Device struct in crates/openlogi-hidpp/src/device.rs wraps a HID++ node—whether a mouse, keyboard, or receiver—and provides access to the root feature and supported capabilities. Each device instance maintains a reference to the underlying HidppChannel and exposes methods for feature enumeration.
Feature Registry System
Central to the implementation is the static map KNOWN_FEATURES in crates/openlogi-hidspp/src/feature/registry.rs. This registry links HID++ feature IDs (e.g., 0x1000 for BatteryStatus, 0x40a0 for SmartShift) to concrete Rust implementations. When a Device initializes, it queries the hardware for supported features and automatically registers the corresponding Rust handlers, enabling type-safe dynamic dispatch.
Protocol Layer Implementation
Wire Format Definitions
OpenLogi explicitly defines HID++ wire formats in version-specific modules:
- HID++ 1.0: Legacy support in
crates/openlogi-hidpp/src/protocol/v10.rs - HID++ 2.0: Modern implementation in
crates/openlogi-hidpp/src/protocol/v20.rs
These modules contain constants for report IDs (0x10 for short, 0x11 for long), byte offsets, and helper functions for parsing little-endian multi-byte fields.
Feature Implementations
Individual feature modules in crates/openlogi-hidpp/src/feature/*.rs implement the Feature trait for specific capabilities:
smartshift.rscontrols ratchet wheel behaviorbattery_status.rsreports charge levels and charging statebacklight.rsmanages keyboard illumination
Each implementation handles feature-specific message construction and response parsing while utilizing the shared HidppChannel for transport.
How the HID++ Protocol Flow Works
The implementation follows a strict lifecycle for device communication:
- Transport Initialization: Create a raw channel implementing
RawHidChannel - Protocol Upgrade:
HidppChannel::from_raw_channelconsumes the transport, validates HID++ support, and spawns background reader tasks - Software-ID Selection: Configure
SwIdPolicy(defaultFixed(1), orrotating()for multi-process safety) to assign 4-bit transaction identifiers - Message Exchange: Use
channel.send()to transmit requests; the internalpending_messagesHashMap tracks outstanding requests and matches responses by software ID - Receiver Detection: The
receivermodule identifies Logitech Bolt or Unifying receivers and enumerates attached nodes - Feature Discovery:
Device::newqueries feature indices and binds them to registry entries, enabling typed access viaget_feature::<T>()
Practical Implementation Example
The following example demonstrates initializing a HID++ connection and interacting with device features using the OpenLogi API:
use std::sync::Arc;
use openlogi_hidpp::{
channel::{HidppChannel, SwIdPolicy},
device::Device,
feature::{BatteryStatusFeature, SmartShiftFeature},
receiver,
nibble::U4,
};
#[tokio::main]
async fn main() {
// 1️⃣ Provide a raw HID channel (implements `RawHidChannel`)
let raw_hid = my_async_hid_impl();
// 2️⃣ Upgrade to HID++ channel
let mut channel = HidppChannel::from_raw_channel(raw_hid)
.await
.expect("HID++ not supported");
// 3️⃣ Configure software-ID policy (optional)
channel.set_sw_id_policy(SwIdPolicy::rotating());
let channel = Arc::new(channel);
// 4️⃣ Detect Logitech receiver (Bolt or Unifying)
let receiver = receiver::detect(Arc::clone(&channel))
.expect("No Logitech receiver found");
// 5️⃣ Enumerate attached devices
let devices = receiver.enumerate_devices().await.unwrap();
// 6️⃣ Initialize first device
let mut dev = Device::new(Arc::clone(&channel), devices[0].index)
.await
.expect("Failed to initialise device");
// 7️⃣ Ping root feature to verify connectivity
let root = dev.root();
assert_eq!(root.ping(0x2).await.unwrap(), 0x2);
// 8️⃣ Access BatteryStatus feature
let battery = dev
.get_feature::<BatteryStatusFeature>()
.expect("Battery feature missing");
let status = battery.status().await.unwrap();
println!("Battery level: {} %", status.level);
// 9️⃣ Control SmartShift if available
if let Some(smartshift) = dev.get_feature::<SmartShiftFeature>() {
smartshift.set_enabled(true).await.unwrap();
}
}
This example mirrors the quick-start documentation in crates/openlogi-hidpp/src/lib.rs (lines 27-78), demonstrating the async pattern for device enumeration and feature access.
Key Source Files
The implementation spans multiple crates and modules:
crates/openlogi-hidpp/src/channel.rs: CoreHidppChannelwith message lifecycle managementcrates/openlogi-hidpp/src/device.rs: High-levelDeviceabstraction and feature discoverycrates/openlogi-hidpp/src/feature/registry.rs: StaticKNOWN_FEATURESmap for capability dispatchcrates/openlogi-hidpp/src/protocol/v20.rs: HID++ 2.0 wire format structurescrates/openlogi-hid/src/transport/native.rs:RawHidChanneltrait definition for transport abstraction
Summary
- OpenLogi provides a complete Rust implementation of Logitech's HID++ protocol through the
openlogi-hidppcrate, eliminating dependencies on proprietary SDKs. - The layered architecture separates raw HID transport (
RawHidChannel), protocol framing (HidppChannel), and device capabilities (Device+ feature registry). - Software-ID policies (
SwIdPolicy) prevent transaction collisions when multiple processes access the same peripheral. - Automatic feature discovery via the
KNOWN_FEATURESregistry enables type-safe access to device-specific capabilities like SmartShift and BatteryStatus. - Support for both HID++ 1.0 and 2.0 ensures compatibility with legacy and modern Logitech hardware.
Frequently Asked Questions
What is the HID++ protocol used by OpenLogi?
HID++ is Logitech's proprietary extension to the standard USB HID protocol, used by modern mice, keyboards, and receivers to expose advanced features like configurable DPI, smart scrolling, and battery monitoring. OpenLogi implements this protocol to enable direct hardware control without Logitech's proprietary software, accessing features through raw HID reports defined in crates/openlogi-hidpp/src/protocol/v20.rs.
How does OpenLogi handle different HID++ protocol versions?
The implementation detects protocol capabilities during channel initialization in HidppChannel::from_raw_channel. Separate modules in crates/openlogi-hidpp/src/protocol/ define version-specific constants and parsing logic for HID++ 1.0 (v10.rs) and 2.0 (v20.rs), with the channel automatically selecting the appropriate message format based on device responses.
What are software-ID policies in OpenLogi's implementation?
Software-ID policies manage the 4-bit transaction identifiers in HID++ messages to prevent collisions when multiple software components communicate with the same device. The default SwIdPolicy::Fixed(1) assigns a static ID, while SwIdPolicy::rotating() cycles through available IDs, and leased policies allow temporary allocation for specific operations—all configured via channel.set_sw_id_policy().
How does feature discovery work in the OpenLogi HID++ implementation?
When a Device initializes via Device::new, it queries the hardware for supported feature indices and cross-references them against the static KNOWN_FEATURES registry in src/feature/registry.rs. This enables runtime capability detection: calling device.get_feature::<BatteryStatusFeature>() returns Some(feature) only if the hardware reports support for that feature ID, providing type-safe access to device-specific functionality.
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 →