How OpenLogi Handles HID++ Device Communication: Architecture and Implementation

OpenLogi handles HID++ device communication through a layered async architecture that abstracts raw HID transport into type-safe feature APIs using the hidpp protocol crate.

In the AprilNEA/OpenLogi repository, HID++ communication is implemented across four specialized crates that separate transport mechanics from high-level device control. This design enables robust async communication with Logitech peripherals while maintaining clean separation between protocol-level operations and application logic.

Architecture Overview

The codebase organizes HID++ communication into distinct layers, each with specific responsibilities and implementation files.

Transport Layer

The transport layer handles raw device detection and channel initialization. In crates/openlogi-hid/src/transport/native.rs, the system enumerates USB and BLE devices using enumerate_devices, filters for HID++ capability via is_hidpp_node, and establishes communication channels through open_hidpp_channel.

This layer creates a HidppChannel struct that wraps the raw HID device handle, providing the foundation for all higher-level operations.

Channel Layer

Located in crates/openlogi-hidpp/src/channel.rs, the channel layer implements the HidppChannel struct with async request-response semantics. Key capabilities include:

  • Protocol adaptation: Automatically detects short vs. long report support via supports_short_long_hidpp
  • Message sequencing: Adds sequence numbers to outgoing reports
  • Error resilience: Implements transparent retry logic for transient failures
  • Tracing integration: Instruments all HID++ transactions for debugging

The channel exposes a unified request method that accepts feature-specific request objects and returns decoded responses.

Feature API Layer

HID++ features are defined in crates/openlogi-hidpp/src/feature/*.rs as strongly-typed structs representing specific device capabilities like DPI control, backlighting, and SmartShift. The #[derive(Feature)] procedural macro in crates/openlogi-hidpp-derive/src/lib.rs generates the encoding and decoding logic for each feature, mapping Rust types to HID++ function IDs and payload layouts.

Device Layer

High-level device sessions reside in crates/openlogi-device/src/, where modules like write/dpi.rs and session/keyboard.rs orchestrate feature calls. These modules instantiate feature structs, populate payloads, and forward commands through the channel abstraction.

The Communication Flow

OpenLogi establishes HID++ communication through a five-step process:

  1. Device Enumeration: The HID transport scans system devices and applies is_hidpp_node to filter for Logitech HID++ capable peripherals based on usage page and ID criteria.

  2. Channel Initialization: For each valid device, open_hidpp_channel constructs a RawHidChannel and upgrades it to an async HidppChannel, querying the device to determine short or long report support.

  3. Request Construction: Application code creates feature-specific requests using generated structs (e.g., Dpi::set(800)), which implement the Feature trait for automatic serialization.

  4. Protocol Transmission: The channel's request method converts requests to HID++ reports, handles message length adaptation (short to long conversion when needed), writes to the device, and awaits the response.

  5. Response Handling: Incoming reports are deserialized into strongly-typed response structs, with errors triggering the transport layer's retry mechanisms as implemented in crates/openlogi-hid/src/transport.rs.

Implementing HID++ Features

The feature system uses Rust's procedural macros to eliminate boilerplate. When you apply #[derive(Feature)] to a struct in crates/openlogi-hidpp/src/feature/, the macro expands to implement:

  • Function ID mapping: Associates the struct with its HID++ feature index and function number
  • Payload encoding: Converts Rust fields into HID++ report bytes according to the protocol specification
  • Response parsing: Deserializes device replies into Rust types

For example, the DPI feature in crates/openlogi-hidpp/src/feature/dpi.rs uses this macro to generate methods like Dpi::get() and Dpi::set(dpi: u16) that handle the binary protocol details automatically.

Practical Examples

Enumerating and Connecting to HID++ Devices

use openlogi_hid::transport::Transport;
use openlogi_hid::transport::enumerate_hidpp;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Scan all HID devices and filter for HID++ capable ones
    let hidpp_nodes = enumerate_hidpp().await?;
    
    for node in hidpp_nodes {
        println!("Found HID++ device: {:?}", node);
        
        // Open an async HID++ channel for this node
        let channel = node.open_hidpp().await?;
        
        // Query the device's feature set
        let features = channel.request(
            openlogi_hidpp::feature::FeatureSet::get()
        ).await?;
        
        println!("Supported features: {:?}", features);
    }
    Ok(())
}

Setting Device DPI via the Feature API

use openlogi_hidpp::feature::Dpi;
use openlogi_hidpp::channel::HidppChannel;

async fn set_device_dpi(
    channel: &HidppChannel, 
    dpi: u16
) -> anyhow::Result<()> {
    // Build a DPI-set request using the generated Feature implementation
    let request = Dpi::set(dpi);
    
    // Send the request and ignore the empty response
    channel.request(request).await?;
    Ok(())
}

Summary

Frequently Asked Questions

How does OpenLogi detect if a device supports HID++?

OpenLogi detects HID++ devices in crates/openlogi-hid/src/transport/native.rs using the is_hidpp_node function, which inspects the USB or BLE usage page and usage ID to identify Logitech HID++ compatible peripherals during enumeration.

What is the difference between short and long HID++ reports?

Short reports use a compact 7-byte format for simple commands, while long reports support 20-byte payloads for complex feature data. The HidppChannel in crates/openlogi-hidpp/src/channel.rs automatically queries the device via supports_short_long_hidpp and adapts messages accordingly, converting short to long format when necessary.

How does the Feature derive macro work?

The #[derive(Feature)] procedural macro defined in crates/openlogi-hidpp-derive/src/lib.rs parses feature struct definitions and generates implementations for the Feature trait, including methods to encode requests into HID++ report bytes and decode device responses back into Rust types.

Can OpenLogi handle multiple HID++ devices simultaneously?

Yes. The architecture supports concurrent device sessions through independent HidppChannel instances. Each device connection maintains its own async channel, and the transport layer uses tokio for non-blocking I/O across multiple peripherals.

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 →