# How OpenLogi Handles HID++ Device Enumeration and Probing Across macOS, Linux, and Windows

> Discover how OpenLogi masterfully handles HID++ device enumeration and probing across macOS Linux and Windows utilizing its unique three-layer architecture and cached feature detection.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: internals
- Published: 2026-08-30

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-hid/src/transport/mod.rs). This function validates the HID usage page and usage ID against the HID++ collection identifier (`0xFF43`):

```rust
// 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:

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-device/src/write.rs), the DPI setting flow demonstrates this pattern:

```rust
// 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:

```rust
// 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 and `WindowsBackend` (winapi) for Windows, both implementing the `HidBackend` trait for consistent enumeration.
- **Protocol filtering**: The `is_hidpp_node` function filters raw HID nodes by usage page `0xFF43` before HID++ processing.
- **Channel caching**: `HidppChannel` instances are opened once per device and cached via `Arc` to prevent repeated initialization overhead.
- **Feature probing**: `enumerate_features` queries device capabilities and returns a `FeatureSet` used by high-level APIs for DPI, lighting, and battery monitoring.
- **Device layer**: `openlogi-device` caches probe results and validates feature support before executing commands like `set_dpi` or 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.