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

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

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/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 identifies devices speaking HID++:

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:

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/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/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
HidppChannel Async request/response, error handling, format conversion src/channel.rs
Message Wire-format encoding/decoding of HID++ structs 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

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/master/crates/openlogi-hidpp/src/feature/dpi.rs); usage in [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:

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:

Windows-Specific Transport Implementation

OpenLogi provides platform-specific optimizations. The Windows transport in 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/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 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)
// 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/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 performs basic HID report read/write operations and detects short/long report support. HidppChannel in 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, 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →