# HID++ Protocol Implementation in OpenLogi for Logitech Devices

> Explore OpenLogi's HID++ protocol implementation in Rust. Communicate with Logitech devices using message framing, ID management, and feature discovery for mice, keyboards, and receivers.

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

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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_messages` queue
- **Asynchronous I/O** with background read loops
- **Software-ID policies** via the `SwIdPolicy` enum 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/protocol/v10.rs)
- **HID++ 2.0**: Modern implementation in [`crates/openlogi-hidpp/src/protocol/v20.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/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.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/smartshift.rs) controls ratchet wheel behavior
- [`battery_status.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/battery_status.rs) reports charge levels and charging state
- [`backlight.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/backlight.rs) manages 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:

1. **Transport Initialization**: Create a raw channel implementing `RawHidChannel`
2. **Protocol Upgrade**: `HidppChannel::from_raw_channel` consumes the transport, validates HID++ support, and spawns background reader tasks
3. **Software-ID Selection**: Configure `SwIdPolicy` (default `Fixed(1)`, or `rotating()` for multi-process safety) to assign 4-bit transaction identifiers
4. **Message Exchange**: Use `channel.send()` to transmit requests; the internal `pending_messages` HashMap tracks outstanding requests and matches responses by software ID
5. **Receiver Detection**: The `receiver` module identifies Logitech Bolt or Unifying receivers and enumerates attached nodes
6. **Feature Discovery**: `Device::new` queries feature indices and binds them to registry entries, enabling typed access via `get_feature::<T>()`

## Practical Implementation Example

The following example demonstrates initializing a HID++ connection and interacting with device features using the OpenLogi API:

```rust
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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/channel.rs): Core `HidppChannel` with message lifecycle management
- [`crates/openlogi-hidpp/src/device.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/device.rs): High-level `Device` abstraction and feature discovery
- [`crates/openlogi-hidpp/src/feature/registry.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/feature/registry.rs): Static `KNOWN_FEATURES` map for capability dispatch
- [`crates/openlogi-hidpp/src/protocol/v20.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/protocol/v20.rs): HID++ 2.0 wire format structures
- [`crates/openlogi-hid/src/transport/native.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hid/src/transport/native.rs): `RawHidChannel` trait definition for transport abstraction

## Summary

- **OpenLogi** provides a complete Rust implementation of Logitech's HID++ protocol through the `openlogi-hidpp` crate, 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_FEATURES` registry 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/v10.rs)) and 2.0 ([`v20.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.