How OpenLogi Communicates with Logitech Devices via Bolt, Unifying, Bluetooth, and Wired Connections

OpenLogi communicates with Logitech peripherals through the HID++ protocol across three architectural layers: a transport layer that enumerates HID devices, a channel layer that wraps raw reports into structured messages, and receiver-specific implementations for Bolt and Unifying hardware, with direct USB and Bluetooth connections handled via the same protocol stack.

OpenLogi is an open-source Rust implementation for managing Logitech peripherals without proprietary software. The project, hosted at AprilNEA/OpenLogi, implements the HID++ protocol to enable communication with devices connected via Bolt receivers, Unifying receivers, or directly through USB and Bluetooth Low Energy.

The Three-Layer Communication Architecture

OpenLogi's communication stack is organized into three distinct layers that abstract the complexity of Logitech's proprietary protocols.

The transport layer resides in crates/openlogi-hid/src/transport.rs and handles physical device detection. It filters system HID nodes to identify Logitech vendor ID 0x046D and specific usage page combinations that indicate HID++ capability.

The HID++ channel layer in crates/openlogi-hidpp/src/channel.rs wraps raw HID reports into structured request/response messages. This layer manages the protocol's short reports (0x10) and long reports (0x11), providing methods like read_register and write_register for device communication.

The receiver abstraction layer implements protocol-specific behavior for Bolt and Unifying hardware in crates/openlogi-hidpp/src/receiver/bolt.rs and crates/openlogi-hidpp/src/receiver/unifying.rs.

Transport Layer: Enumerating Logitech HID Devices

The transport layer begins communication by discovering available HID nodes through the enumerate_devices function defined in crates/openlogi-hid/src/transport.rs.

Detecting HID++ Long Collections

The enumeration process filters for Logitech's specific usage page and usage ID combinations that indicate HID++ long-report support:

  • USB receivers: Usage page 0xFF00, usage ID 0x0002
  • Bluetooth Low Energy: Usage page 0xFF43, usage ID 0x0202
  • Wired keyboards: Usage page 0xFF43, usage ID 0x0602

The function is_hidpp_long_collection validates these combinations to identify compatible devices.

let all: Vec<async_hid::Device> = HID_BACKEND.enumerate().await?;
for d in all.iter().filter(|d| d.vendor_id == LOGITECH_VENDOR_ID) {
    debug!(
        name = %d.name,
        pid = format_args!("{:04x}", d.product_id),
        usage_page = format_args!("{:#06x}", d.usage_page),
        usage_id = format_args!("{:#06x}", d.usage_id),
        matched = is_hidpp_long_collection(d.usage_page, d.usage_id),
        "logitech HID node"
    );
}

Filtering Receiver Child Nodes

When Logitech receivers connect to Linux systems, the hid-logitech-dj kernel driver creates per-device child nodes under /dev/hidraw. The is_receiver_child_node function in transport.rs discards these child nodes because communication must occur through the receiver node itself, not individual device endpoints.

Channel Layer: Wrapping HID Reports

Once a valid HID node is identified, the system constructs a HidppChannel from crates/openlogi-hidpp/src/channel.rs. This channel abstracts the raw byte-level communication into structured HID++ operations.

Device Index Handling

The channel initialization accepts a device index parameter. For receiver-based communication, this uses RECEIVER_DEVICE_INDEX = 0xff. For direct connections, the same index value applies, indicating the device communicates without a receiver intermediary.

let chan = HidppChannel::new(device, RECEIVER_DEVICE_INDEX).await?;

Register Operations

The HidppChannel implementation exposes three primary methods for device interaction:

  • read_register: Reads short-form register values
  • write_register: Writes data to device registers
  • read_long_register: Retrieves extended data using long-report format (0x11)

Additionally, the channel registers a message listener that decodes asynchronous HID++ notifications and forwards them to an EventEmitter, enabling real-time event handling for device connections and disconnections.

Receiver Implementations: Bolt and Unifying

OpenLogi provides distinct receiver implementations that share a common trait interface but handle protocol-specific register layouts.

Bolt Receiver Protocol

The Bolt receiver implementation in crates/openlogi-hidpp/src/receiver/bolt.rs manages devices paired through Logitech's newer Bolt wireless protocol. The Receiver struct exposes several high-level methods:

  • get_notification_state: Reads register 0x00 to determine enabled wireless notifications
  • count_pairings: Reads register 0x02 to retrieve the number of occupied pairing slots
  • get_unique_id: Reads register 0xFB to obtain the receiver's unique identifier (distinct from serial number)
  • trigger_device_arrival: Writes to register 0xC0 to force emission of arrival events for all paired slots, enabling device enumeration
  • discover_devices / cancel_device_discovery: Manages BLE-based device discovery using register 0xC0

The Bolt implementation follows the HID++ 1.0 specification based on reverse-engineered data, as noted in the source comments.

Unifying Receiver Protocol

The Unifying receiver in crates/openlogi-hidpp/src/receiver/unifying.rs provides equivalent functionality but with register layout differences specific to legacy Unifying hardware. For example, device codenames store at register 0x40 + n-1 rather than the Bolt base of 0x60.

Key methods mirror the Bolt implementation:

  • count_pairings targets register 0x02
  • set_wireless_notifications manipulates register 0x00
  • trigger_device_arrival writes [0x02, 0x00, 0x00] to register 0x02
  • get_receiver_info reads long register 0xB5 with sub-register 0x03

Both receivers implement the Receiver trait defined in crates/openlogi-hidpp/src/receiver.rs, allowing uniform treatment by higher-level application logic.

Direct Connections: USB and Bluetooth

For devices communicating without a receiver, OpenLogi uses the Direct connection type defined in crates/openlogi-core/src/hid/route.rs.

The DeviceRoute Abstraction

The DeviceRoute enum distinguishes connection types:

pub enum DeviceRoute {
    Bolt { receiver_uid: String, slot: u8 },
    Unifying { receiver_uid: String, slot: u8 },
    Direct { /* no receiver */ },
}

Direct connections occur when the enumerated HID node is not a child of a known receiver, as determined by is_hidpp_node in transport.rs. These devices—whether connected via USB cable or Bluetooth Low Energy—use the same HidppChannel implementation with device index 0xFF, maintaining protocol consistency across all connection types.

Bluetooth Low Energy Handling

BLE devices present usage page 0xFF43 and usage ID 0x0202. On macOS, the system requires CoreBluetooth permissions handled in crates/openlogi-permissions/src/macos.rs, while Linux implementations filter receiver child nodes through sysfs examination.

Practical Implementation Examples

Opening a Bolt Receiver and Enumerating Devices

The following example demonstrates discovering a Bolt receiver, establishing a channel, and triggering device arrival notifications:

use openlogi_hidpp::receiver::bolt::Receiver;
use openlogi_hidpp::channel::HidppChannel;
use std::sync::Arc;

// Enumerate HID nodes and locate Bolt receiver
let devices = openlogi_hid::enumerate_devices().await?;
let bolt_dev = devices.into_iter()
    .find(|d| d.vendor_id == LOGITECH_VENDOR_ID && d.product_id == BOLT_PID)
    .expect("No Bolt receiver found");

// Establish HID++ channel with receiver device index
let chan = HidppChannel::new(bolt_dev, RECEIVER_DEVICE_INDEX).await?;

// Initialize Bolt receiver abstraction
let bolt = Receiver::new(Arc::new(chan)).expect("Failed to initialise Bolt");

// Query receiver information
let uid = bolt.get_unique_id().await?;
println!("Bolt UID: {}", uid);

let count = bolt.count_pairings().await?;
println!("Paired devices: {}", count);

// Trigger arrival notifications for enumeration
bolt.trigger_device_arrival().await?;

// Listen for device events
let mut events = bolt.listen();
while let Ok(event) = events.recv().await {
    println!("Device event: {:?}", event);
}

Discovering New BLE Peripherals

To discover new devices for pairing:

// Initiate discovery mode
bolt.discover_devices().await?;

// Process discovery events
let mut ev = bolt.listen();
while let Ok(event) = ev.recv().await {
    match event {
        DeviceConnection::Discovered { address, name, .. } => {
            println!("Found device {} ({})", name, address);
        }
        DeviceConnection::PasskeyPress { .. } => {
            // Handle passkey confirmation
        }
        _ => {}
    }
}

Connecting Direct Bluetooth Devices

For BLE devices without a receiver:

use openlogi_hidpp::channel::HidppChannel;
use openlogi_hid::enumerate_devices;

let devices = enumerate_devices().await?;
let bt_device = devices.into_iter()
    .find(|d| d.vendor_id == LOGITECH_VENDOR_ID && d.usage_page == 0xFF43 && d.usage_id == 0x0202)
    .expect("No BLE device found");

// Create direct channel with index 0xFF
let chan = HidppChannel::new(bt_device, 0xFF).await?;

// Read device registers directly
let uid = chan.read_long_register(0xFF, Register::UniqueId.into(), [0, 0, 0]).await?;
println!("Device UID: {}", hex::encode_upper(&uid[1..=4]));

Summary

OpenLogi enables communication with Logitech devices through a layered architecture implementing the HID++ protocol:

  • Transport Layer (crates/openlogi-hid/src/transport.rs): Detects Logitech HID nodes by vendor ID 0x046D and usage page combinations, filtering for long-report collections while excluding receiver child nodes
  • Channel Layer (crates/openlogi-hidpp/src/channel.rs): Wraps raw HID reports into structured messages, providing read_register, write_register, and read_long_register methods with asynchronous event handling
  • Receiver Layer (crates/openlogi-hidpp/src/receiver/bolt.rs and unifying.rs): Implements protocol-specific register maps for Bolt and Unifying receivers, exposing high-level discovery and pairing methods
  • Direct Connections (crates/openlogi-core/src/hid/route.rs): Handles USB and Bluetooth devices using the same HID++ channel with device index 0xFF, abstracting connection differences through the DeviceRoute enum

Frequently Asked Questions

What is the HID++ protocol and how does OpenLogi implement it?

The HID++ protocol is Logitech's proprietary extension to standard HID for advanced device management. OpenLogi implements this protocol through the HidppChannel struct in crates/openlogi-hidpp/src/channel.rs, which wraps raw HID reports into structured request/response messages. The implementation supports both short reports (0x10) and long reports (0x11), enabling register reads, writes, and asynchronous event notifications across all connection types.

How does OpenLogi distinguish between Bolt and Unifying receivers?

OpenLogi distinguishes receivers by product ID and register layout. During enumeration in transport.rs, the system identifies specific product IDs associated with Bolt hardware. The Receiver implementations in crates/openlogi-hidpp/src/receiver/bolt.rs and unifying.rs then handle protocol differences: Bolt uses register base 0x60 for device codenames and register 0xC0 for discovery, while Unifying uses base 0x40 and register 0x02 for similar operations. Both implement the common Receiver trait for uniform API access.

Can OpenLogi communicate with Logitech devices without a receiver?

Yes. OpenLogi communicates directly with USB-connected and Bluetooth Low Energy devices through the Direct variant of the DeviceRoute enum. These connections use the same HidppChannel infrastructure with device index 0xFF, bypassing receiver-specific logic while maintaining full HID++ protocol compatibility for register operations and event handling.

What are the specific usage page values for identifying Logitech HID++ devices?

OpenLogi identifies HID++ capable devices through specific usage page and usage ID combinations defined in crates/openlogi-hid/src/transport.rs:

  • USB receivers: usage page 0xFF00, usage ID 0x0002
  • Bluetooth Low Energy: usage page 0xFF43, usage ID 0x0202
  • Wired keyboards: usage page 0xFF43, usage ID 0x0602

The is_hidpp_long_collection function validates these combinations to filter compatible Logitech devices from system HID enumerations.

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 →