How OpenLogi Handles HID++ Device Enumeration and Probing Across macOS, Linux, and Windows
OpenLogi implements a three-layer architecture—transport, HID++ backend, and device abstraction—to enumerate and probe Logitech HID++ devices across macOS, Linux, and Windows using OS-specific backends and cached feature detection.
OpenLogi is an open-source Rust project that unifies Logitech device management across platforms. The codebase separates HID++ device enumeration into distinct transport backends for each operating system, with shared logic for protocol filtering and feature probing. This design ensures consistent device discovery whether running on macOS, Linux, or Windows.
OS-Specific Transport Layer Implementation
The transport layer handles raw HID node enumeration and provides platform-specific implementations for interacting with the host HID stack. OpenLogi maintains separate backends for Unix-like systems and Windows to abstract OS API differences.
Native Backend for macOS and Linux
On macOS and Linux, OpenLogi uses the async-hid crate through the NativeBackend struct defined in openlogi-hid/src/transport/native.rs. The backend maintains a process-wide singleton (NATIVE_BACKEND) that caches device handles to avoid repeated OS queries.
The enumerate_hidpp method refreshes the device cache and filters for HID++ capable nodes:
// openlogi-hid/src/transport/native.rs
impl HidBackend for NativeBackend {
async fn enumerate(&self) -> Result<Vec<NodeInfo>, BackendError> {
Ok(self
.refresh()
.await?
.iter()
.map(|device| super::node_info(device))
.collect())
}
async fn enumerate_hidpp(&self) -> Result<Vec<NodeInfo>, BackendError> {
Ok(self
.refresh()
.await?
.iter()
.filter(|device| is_hidpp_node(device))
.map(|device| super::node_info(device))
.collect())
}
}
This implementation converts async_hid::Device handles into NodeInfo structs containing vendor ID, product ID, and device identifiers for further processing.
Windows Backend Implementation
Windows uses a dedicated WindowsBackend in openlogi-hid/src/transport/windows_hid.rs that interfaces with the Windows HID API via winapi bindings. Like its native counterpart, it exposes enumerate_hidpp but adapts the filtering logic for Windows-specific device enumeration:
// openlogi-hid/src/transport/windows_hid.rs
impl HidBackend for WindowsBackend {
async fn enumerate_hidpp(&self) -> Result<Vec<NodeInfo>, BackendError> {
let devices = self.enumerate_devices().await?;
Ok(devices
.into_iter()
.filter(|dev| is_hidpp_node(dev))
.map(|dev| node_info(&dev))
.collect())
}
}
Both backends share the same HidBackend trait interface, allowing higher-level code to remain platform-agnostic while leveraging OS-specific optimizations.
HID++ Protocol Filtering
After raw enumeration, OpenLogi filters for HID++ compatible devices using the is_hidpp_node function in openlogi-hid/src/transport/mod.rs. This function validates the HID usage page and usage ID against the HID++ collection identifier (0xFF43):
// openlogi-hid/src/transport/mod.rs
pub fn is_hidpp_node(device: &Device) -> bool {
device.usage_page == HIDPP_USAGE_PAGE && device.usage_id == HIDPP_USAGE_ID
}
Only devices matching this signature proceed to the HID++ backend for feature enumeration, preventing non-Logitech or non-HID++ devices from entering the probing pipeline.
Feature Probing and Channel Management
The openlogi-hidpp crate manages protocol-level communication and capability detection. Once a transport node is identified as HID++ capable, the backend opens a dedicated channel and queries the device's feature set.
Opening HID++ Channels
Channel establishment occurs through open_hidpp_channel, implemented in the transport layer but managed by the HID++ backend. The NativeBackend provides access via open_hidpp, which creates a reusable HidppChannel wrapped in an Arc for thread-safe caching:
// openlogi-hid/src/transport/native.rs
async fn open_hidpp(&self, node: &NodeInfo) -> Result<Option<Arc<HidppChannel>>, BackendError> {
let device = self.handle(node)?;
open_hidpp_channel(&device, self.device_io.clone()).await
}
Channels are cached per node, ensuring subsequent operations reuse the same connection without re-initializing the HID interface.
Enumerating Device Features
The HidppDevice struct in openlogi-hidpp/src/device.rs exposes enumerate_features, which sends HID++ "Feature Set" requests to build a capability bitmap. This method returns a FeatureSet describing supported functions like DPI adjustment, lighting control, and SmartShift:
// openlogi-hidpp/src/device.rs
pub async fn enumerate_features(&self) -> Result<FeatureSet, BackendError> {
// Sends HID++ "Feature Set" request, parses the bitmap,
// and builds a `FeatureSet` describing capabilities (lighting, DPI, SmartShift, …).
}
This probing step occurs once per device connection and caches results to avoid repeated protocol handshakes during normal operation.
High-Level Device Abstraction and Caching
The openlogi-device crate provides the final abstraction layer, wrapping transport and HID++ functionality into ergonomic device objects. When applications interact with devices, the library ensures probing completes before executing commands.
The probe cache (openlogi_device::probe::Cache) stores discovered capabilities, enabling methods like set_dpi to validate feature availability without re-enumerating. From openlogi-device/src/write.rs, the DPI setting flow demonstrates this pattern:
// openlogi-device/src/write.rs
pub async fn set_dpi(&self, dpi: Dpi) -> Result<(), DeviceError> {
let hidpp = self.backend.open_hidpp(&self.node).await?;
let features = hidpp.enumerate_features().await?;
// Validate that DPI feature is present, then send the appropriate report.
}
This architecture ensures HID++ device enumeration results inform all subsequent interactions while minimizing redundant protocol traffic.
Cross-Platform Orchestration
The openlogi-agent crate orchestrates the entire stack using a global HID_BACKEND singleton that selects the appropriate implementation (native for macOS/Linux, windows for Windows) at compile time. The agent runs a hot-plug watcher (watch_nodes) that triggers fresh enumeration when devices connect or disconnect, maintaining an accurate roster of available HID++ peripherals across all supported platforms.
Platform-agnostic code can enumerate devices using the unified backend interface:
// Example: List all HID++ devices (platform-agnostic)
use openlogi_hid::backend::native_backend;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let backend = native_backend();
let hidpp_nodes = backend.enumerate_hidpp().await?;
for node in hidpp_nodes {
println!("Found HID++ device: {} ({:04x}:{:04x})",
node.name, node.vendor_id, node.product_id);
}
Ok(())
}
Summary
- Transport abstraction: OpenLogi uses
NativeBackend(async-hid) for macOS/Linux andWindowsBackend(winapi) for Windows, both implementing theHidBackendtrait for consistent enumeration. - Protocol filtering: The
is_hidpp_nodefunction filters raw HID nodes by usage page0xFF43before HID++ processing. - Channel caching:
HidppChannelinstances are opened once per device and cached viaArcto prevent repeated initialization overhead. - Feature probing:
enumerate_featuresqueries device capabilities and returns aFeatureSetused by high-level APIs for DPI, lighting, and battery monitoring. - Device layer:
openlogi-devicecaches probe results and validates feature support before executing commands likeset_dpior lighting adjustments.
Frequently Asked Questions
How does OpenLogi differentiate between regular HID devices and HID++ devices?
OpenLogi checks the HID usage page and usage ID in the is_hidpp_node function located in openlogi-hid/src/transport/mod.rs. Specifically, it validates that the device reports usage page 0xFF43 and the corresponding HID++ usage ID, which identifies Logitech's proprietary protocol extension. Only devices matching this signature proceed to feature enumeration.
Why does OpenLogi use separate backends for Windows and macOS/Linux?
The separation allows OpenLogi to leverage optimal OS-specific HID APIs. macOS and Linux use the async-hid crate for asynchronous device access, while Windows requires the winapi crate to interface with the native Windows HID subsystem. Both backends implement the same HidBackend trait, ensuring the rest of the codebase remains platform-agnostic while handling OS-specific enumeration quirks.
What happens during the HID++ feature probing phase?
The enumerate_features method in openlogi-hidpp/src/device.rs sends a HID++ "Feature Set" request to the device, which returns a bitmap of supported capabilities. OpenLogi parses this bitmap to construct a FeatureSet describing available functions such as DPI control, battery monitoring, lighting effects, and SmartShift. These results are cached to avoid repeated protocol handshakes during device operation.
Can OpenLogi handle hot-plugging of HID++ devices?
Yes. The openlogi-agent crate implements a hot-plug watcher (watch_nodes) that monitors the system for device connection and disconnection events. When a change is detected, the agent triggers a fresh HID++ device enumeration through the global HID_BACKEND singleton, ensuring the device roster stays current without requiring application restarts.
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 →