# How OpenLogi Implements the HID++ Protocol: From Device Registry to the HID Layer

> Explore OpenLogi's HID++ protocol implementation. Discover its three-layer architecture from device registry to HID layer for efficient hardware identification and command execution.

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

---

**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](https://github.com/AprilNEA/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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device-registry/src/lib.rs):

```rust
pub const LOGITECH_VENDOR_ID: u16 = 0x046d;

```

The registry organizes hardware into sub-modules:

- `litra` – Product IDs for Litra lighting devices
- `receiver` – 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/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hid/src/transport/native.rs) identifies devices speaking HID++:

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

```rust
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/main/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/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/src/channel/raw.rs) |
| `HidppChannel` | Async request/response, error handling, format conversion | [`src/channel.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/src/channel.rs) |
| `Message` | Wire-format encoding/decoding of HID++ structs | [`src/channel/message.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

1. Receives a `Message` from a feature implementation
2. Determines correct report length (short or long)
3. Writes raw bytes via `RawHidChannel::write_report`
4. Awaits response with timeout and retry logic
5. 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**

```rust
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::ReadDpi` request
- Calls `HidppChannel::request` for 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/main/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/main/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:

```rust
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 in [`openlogi-hid/src/transport/native.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-hid/src/transport/native.rs)
- `enumerate_hidpp()` – returns stream of `NodeInfo` structs
- `open_hidpp()` – returns `Option<Arc<HidppChannel>>`
- `DpiFeature::get_dpi()` – typed protocol call in [`openlogi-hidpp/src/feature/dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-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`](https://github.com/AprilNEA/OpenLogi/blob/main/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/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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)

```rust
// 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/main/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-hidpp` runs against mock `RawHidChannel` implementations 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 via `is_hidpp_node`, channel opening
- **HID++ Protocol** (`openlogi-hidpp`) – `RawHidChannel` for raw I/O, `HidppChannel` for async request/response, feature modules for typed APIs
- **Report Format Handling** – Automatic short-to-long conversion based on `supports_short_long_hidpp` detection
- **Entry Points** – `NativeHidBackend::enumerate_hidpp()` and `open_hidpp()` for application developers

## Frequently Asked Questions

### What is the difference between `RawHidChannel` and `HidppChannel`?

`RawHidChannel` in [`crates/openlogi-hidpp/src/channel/raw.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.