HID++ Protocol Implementation Structure in the openlogi-hidpp Crate

The openlogi-hidpp crate implements a layered architecture for HID++ 1.0 and 2.0 communication, separating protocol definitions in src/protocol/, transport channels in src/channel/, device management in src/device.rs and src/receiver/, and feature-specific logic in over 40 src/feature/ modules.

The openlogi-hidpp crate in the AprilNEA/OpenLogi repository provides the low-level Rust driver for Logitech peripherals using the HID++ protocol. Understanding its structure reveals how raw HID bytes transform into type-safe feature interactions across platform-specific transports like async-hid.

Protocol Definition Layer

This layer handles binary message formats. According to the source code, the implementation isolates version-specific parsing to maintain clean separation between HID++ 1.0 and 2.0 specifications while sharing common utilities.

Version-Specific Message Layouts

  • src/protocol/v10.rs: Defines the message header structure and payload layouts for HID++ 1.0 devices, handling the older short-header format with specific bit-field arrangements.
  • src/protocol/v20.rs: Implements the extended header format required for HID++ 2.0, supporting longer payloads and additional protocol flags.
  • src/protocol.rs: Provides common protocol traits and shared helper functions utilized by both version implementations.
  • src/nibble.rs: Contains utilities for manipulating 4-bit integers (U4), essential for packing and unpacking the protocol's dense binary fields where nibbles frequently cross byte boundaries.

Transport and Channel Abstraction

The channel layer abstracts raw HID I/O operations, decoupling the protocol logic from platform-specific backends. This enables the crate to function across macOS, Linux, and Windows without modifying higher-level device code.

Raw Byte Streams

  • src/channel/raw.rs: Implements the raw byte-stream interface that reads from and writes to the underlying HID device file descriptors provided by the OS-specific backend.
  • src/channel.rs: Exposes the high-level Channel struct with send and receive methods, coordinating between the raw transport and protocol parsing layers.
  • src/channel/message.rs: Defines the Message envelope that combines a parsed MessageHeader (originating from v10.rs or v20.rs) with its associated payload bytes for processing.

Asynchronous Observation

  • src/channel/observation.rs: Manages the observation stream for unsolicited reports, such as battery level broadcasts or DPI change notifications initiated by the device rather than the host.

Device and Receiver Management

This layer represents concrete hardware entities and manages their discovery lifecycle through receiver-specific implementations.

The Device Struct

  • src/device.rs: Contains the Device struct, which holds a Channel instance and exposes high-level methods like read_feature and write_feature. Internally, these methods utilize the Emitter to construct commands and delegate response parsing to specific feature modules.

Receiver Implementations

  • src/receiver.rs: Defines the generic Receiver trait for device discovery and pairing operations, establishing a consistent interface for different hardware connection methods.
  • src/receiver/unifying.rs: Implements the Unifying receiver logic for Logitech's wireless dongles, handling device enumeration and connection management protocols specific to the 2.4 GHz proprietary radio.
  • src/receiver/bolt.rs: Provides support for the newer Bolt receiver protocol, including Bluetooth and USB transport modes, with additional event handling logic located in src/receiver/bolt/event.rs.

Feature Module System

Individual HID++ features (battery status, adjustable DPI, SmartShift, RGB effects) reside in dedicated modules under src/feature/. Each module implements the Feature trait, allowing the Device struct to communicate using strongly-typed Rust structs rather than raw byte buffers.

  • Feature Isolation: Files such as src/feature/battery_status.rs, src/feature/adjustable_dpi.rs, and src/feature/smartshift.rs encapsulate the specific HID++ feature ID, request/response data structures, and bitwise encoding logic.
  • Extensibility: Adding support for new HID++ features requires only creating a new module implementing the Feature trait. The architecture supports over 40 distinct features without modifications to the core Device or Channel implementations.

Message Construction and Synchronous Facade

Packet Building

  • src/emitter.rs: Provides the Emitter type for constructing outbound HID++ packets. It handles low-level details of header construction, payload serialization, and checksum calculation required by the protocol specification.

Blocking API

  • src/sync.rs: Offers the SyncDevice wrapper around the asynchronous Device, enabling blocking read_feature and write_feature calls. This facade runs a small internal async runtime to accommodate callers that prefer synchronous APIs while maintaining the crate's async internals.

Implementation Example

The following example demonstrates discovering a device via a Unifying receiver, reading battery status, and adjusting DPI settings using the crate's synchronous facade.

use openlogi_hidpp::{Device, FeatureSet, sync::SyncDevice};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Discover devices via the receiver (e.g., Unifying).
    let receiver = openlogi_hidpp::receiver::unifying::Receiver::new()?;
    let device = receiver
        .devices()
        .await?
        .into_iter()
        .next()
        .ok_or("no device found")?;

    // 2. Wrap the async device in a synchronous façade.
    let mut dev = SyncDevice::new(device);

    // 3. Read the battery status (feature ID 0x10).
    let battery = dev.read_feature::<openlogi_hidpp::feature::battery_status::BatteryInfo>()?;
    println!("Battery level: {} %", battery.level());

    // 4. Change DPI (feature ID 0x07) to 1200 DPI on X-axis.
    let mut dpi = dev.read_feature::<openlogi_hidpp::feature::adjustable_dpi::DpiSettings>()?;
    dpi.set_x(1200);
    dev.write_feature(&dpi)?;

    println!("DPI updated");
    Ok(())
}

Key implementation details:

  • Receiver::new() instantiates a platform-specific receiver (Unifying in this case), while devices() returns an async stream of available Device instances.
  • SyncDevice::new() wraps the async device to provide blocking operations.
  • BatteryInfo and DpiSettings are zero-cost abstractions defined in src/feature/battery_status.rs and src/feature/adjustable_dpi.rs, implementing the Feature trait for type-safe serialization.

Summary

  • Protocol versioning: HID++ 1.0 and 2.0 implementations are isolated in src/protocol/v10.rs and src/protocol/v20.rs to prevent conflicting parsing logic, with shared utilities in src/nibble.rs.
  • Transport abstraction: The Channel struct in src/channel.rs and raw implementation in src/channel/raw.rs decouple the protocol from platform-specific HID backends supported by async-hid.
  • Feature extensibility: Individual modules in src/feature/ implement the Feature trait for each HID++ capability, enabling granular support for device functions without core architectural changes.
  • Synchronous convenience: src/sync.rs provides SyncDevice for blocking APIs while maintaining the crate's asynchronous internals.
  • Receiver support: Both Unifying (src/receiver/unifying.rs) and Bolt (src/receiver/bolt.rs) receivers implement the Receiver trait from src/receiver.rs for diverse connectivity options.

Frequently Asked Questions

How does openlogi-hidpp handle different HID++ protocol versions?

The crate maintains separate modules for each protocol version. src/protocol/v10.rs handles the short-header binary format used by older devices, while src/protocol/v20.rs manages the extended headers of modern devices. Both implement common traits defined in src/protocol.rs, allowing higher-level code in src/device.rs to work with either version transparently through the same Channel abstraction.

What is the role of the Feature trait in the crate architecture?

The Feature trait bridges raw HID++ bytes and Rust data structures. Each module in src/feature/ (such as battery_status.rs or adjustable_dpi.rs) implements this trait for a specific HID++ feature ID, defining how to encode requests and decode responses using the Emitter logic. This enables the Device struct to expose generic read_feature and write_feature methods that return strongly-typed results specific to the requested capability.

Which Logitech receivers are supported by openlogi-hidpp?

The crate supports both legacy and modern receiver protocols. src/receiver/unifying.rs implements the USB dongle protocol used by 2.4 GHz wireless devices, while src/receiver/bolt.rs handles the newer Bolt receiver that operates over Bluetooth and USB. Both implement the Receiver trait from src/receiver.rs for consistent device discovery across transport types.

How are spontaneous device events (like battery notifications) handled?

Asynchronous events are captured through the observation channel implemented in src/channel/observation.rs. When the device sends unsolicited reports (such as battery level changes or DPI adjustments), the raw channel in src/channel/raw.rs parses these into Message structures defined in src/channel/message.rs and forwards them to the observation stream, allowing the application to react to state changes without polling.

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 →