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 ID0x0002 - Bluetooth Low Energy: Usage page
0xFF43, usage ID0x0202 - Wired keyboards: Usage page
0xFF43, usage ID0x0602
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 valueswrite_register: Writes data to device registersread_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 register0x00to determine enabled wireless notificationscount_pairings: Reads register0x02to retrieve the number of occupied pairing slotsget_unique_id: Reads register0xFBto obtain the receiver's unique identifier (distinct from serial number)trigger_device_arrival: Writes to register0xC0to force emission of arrival events for all paired slots, enabling device enumerationdiscover_devices/cancel_device_discovery: Manages BLE-based device discovery using register0xC0
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_pairingstargets register0x02set_wireless_notificationsmanipulates register0x00trigger_device_arrivalwrites[0x02, 0x00, 0x00]to register0x02get_receiver_inforeads long register0xB5with sub-register0x03
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 ID0x046Dand 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, providingread_register,write_register, andread_long_registermethods with asynchronous event handling - Receiver Layer (
crates/openlogi-hidpp/src/receiver/bolt.rsandunifying.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 index0xFF, abstracting connection differences through theDeviceRouteenum
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 ID0x0002 - Bluetooth Low Energy: usage page
0xFF43, usage ID0x0202 - Wired keyboards: usage page
0xFF43, usage ID0x0602
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →