How OpenLogi Discovers Logitech Devices: Bolt, Unifying, Bluetooth, and Wired Connection Pipelines Explained
OpenLogi's device discovery pipeline uses three distinct code paths—Bolt for Bluetooth/wireless receivers, Unifying for 2.4 GHz wireless, and direct HID probing for wired USB—each with dedicated register writes, event parsing, and state aggregation.
The OpenLogi project implements a cross-platform Rust library for managing Logitech peripherals. Its device discovery pipeline handles multiple connection types through specialized receiver implementations, unified behind a common IPC interface that streams discovered devices to the desktop UI.
Bolt Discovery: Bluetooth and Wireless Receiver Scanning
The Logitech Bolt protocol supports both Bluetooth Low Energy and proprietary wireless connections through a unified receiver. Bolt discovery initiates an active scan that emits a stream of device events.
Starting the Bolt Discovery Scan
The scan begins with Receiver::discover_devices in crates/openlogi-hidpp/src/receiver/bolt.rs, which writes the DeviceDiscovery register with a timeout value:
// crates/openlogi-hidpp/src/receiver/bolt.rs
pub async fn discover_devices(&self, timeout: Option<u8>) -> Result<(), ReceiverError> {
self.chan.write_register(
RECEIVER_DEVICE_INDEX,
Register::DeviceDiscovery.into(),
[timeout.unwrap_or(0x00), 0x01, 0x00],
).await?;
Ok(())
}
The byte array [timeout, 0x01, 0x00] activates discovery mode. When timeout is 0x00, the receiver uses its default 30-second scan window.
Parsing Bolt Discovery Events
While discovery is active, the Bolt receiver emits paired events for each detected device. The event parsers live in crates/openlogi-hidpp/src/receiver/bolt/event.rs:
// crates/openlogi-hidpp/src/receiver/bolt/event.rs
fn discovery_counter(payload: &[u8; 17]) -> u16 {
// Extract 16-bit little-endian counter from offset 0-1
u16::from_le_bytes([payload[0], payload[1]])
}
fn discovery_name(payload: &[u8; 17]) -> Option<&str> {
// Parse UTF-8 name from payload offset 2-16
core::str::from_utf8(&payload[2..]).ok()?.split(|b| *b == 0x00).next()
}
Each device generates two event types:
- DeviceDiscoveryDeviceDetails — Contains
kind,product_id, and the linking counter - DeviceDiscoveryDeviceName — Contains the human-readable name and matching counter
The agent matches these frames by counter value, builds a complete DiscoveredDevice struct, and streams it to the GUI over IPC.
Completing Bolt Pairing
After user selection, Receiver::pair_device binds the device to the receiver using the same bolt.rs implementation.
Unifying Discovery: 2.4 GHz Pairing Lock Protocol
Logitech Unifying receivers use a passive pairing model. Instead of active scanning, the receiver opens a pairing lock that accepts any device entering physical pairing mode.
Opening and Closing the Pairing Lock
The lock state is controlled through crates/openlogi-device/src/pairing.rs:
// crates/openlogi-device/src/pairing.rs
pub async fn open_pairing_lock(&self) -> Result<(), ReceiverError> {
self.chan.write_register(
RECEIVER_DEVICE_INDEX,
Register::UnifyingLock.into(),
[0x01, 0x00, 0x00], // 0x01 = open lock
).await?;
Ok(())
}
pub async fn close_pairing_lock(&self) -> Result<(), ReceiverError> {
self.chan.write_register(
RECEIVER_DEVICE_INDEX,
Register::UnifyingLock.into(),
[0x00, 0x00, 0x00], // 0x00 = close lock
).await?;
Ok(())
}
Lock State Notifications
The receiver emits UnifyingLock events defined in crates/openlogi-device/src/pairing/notification.rs:
// crates/openlogi-device/src/pairing/notification.rs
pub struct UnifyingLock {
pub open: bool,
pub error: u8,
}
When open: true and error: 0, the UI displays "pairing mode active." Devices entering pairing mode generate standard DeviceConnection events, which the agent routes to pair_device for final binding.
Wired Discovery: Direct USB HID Enumeration
For wired USB connections, OpenLogi bypasses receivers entirely and probes system HID nodes directly.
HID Transport and Raw Enumeration
The low-level transport layer resides in crates/openlogi-hid/src/transport.rs. On Linux, this interacts with hid-logitech-dj kernel driver nodes; on macOS and Windows, platform-specific HID APIs provide equivalent access.
Probe Cache and Register Reading
The ProbeCache in crates/openlogi-hid/src/probe_cache.rs eliminates redundant device queries. The inventory logic in crates/openlogi-device/src/inventory/probe.rs performs the actual device identification:
- Enumerate all accessible HID raw nodes
- Read
DeviceCodenameandDeviceInformationregisters - Parse codenames using
parse_codenamefrombolt.rs - Map raw
DeviceKindvalues viamap_unifying_kindininventory/mappings.rs
// crates/openlogi-device/src/inventory/probe.rs
pub fn probe_all() -> Result<Vec<Device>, ProbeError> {
let mut devices = Vec::new();
for node in hid_enumerate()? {
if let Some(device) = probe_node(&node, &mut cache)? {
devices.push(device);
}
}
Ok(devices)
}
Unified Device State Management
Regardless of discovery source, all devices converge in DeviceStore at crates/openlogi-desktop/src/state/device_store.rs. This central store:
- Accepts Bolt
DiscoveredDevicestructs from IPC event streams - Processes Unifying lock-state changes and connection events
- Merges wired probe results from
probe_all()calls
The desktop state layer in crates/openlogi-desktop/src/state/devices.rs subscribes to IPC streams and triggers UI refreshes when DeviceStore updates.
Cross-Platform IPC Architecture
The openlogi-ipc crate (crates/openlogi-ipc/src/ipc.rs) defines the contract between GUI and agent:
| RPC Method | Trigger | Response |
|---|---|---|
start_discovery |
User clicks "Add Device" | Stream of DiscoveredDevice events |
cancel_discovery |
User cancels or timeout | Discovery termination |
pair_device |
User selects discovered device | Pairing confirmation or error |
list_devices |
UI refresh or startup | Current DeviceStore snapshot |
Platform-specific implementations are cfg-gated, but the high-level API remains identical across Linux, macOS, and Windows.
Summary
- Bolt discovery uses active scanning via
discover_devicesinbolt.rs, parses paired detail/name events usingdiscovery_counteranddiscovery_namehelpers, and completes withpair_device - Unifying discovery relies on
open_pairing_lockandclose_pairing_lockinpairing.rs, with state notifications throughUnifyingLockevents innotification.rs - Wired discovery probes HID nodes via
probe_allinprobe.rs, leveragingProbeCachefor performance andtransport.rsfor platform abstraction - All paths feed into
DeviceStorefor unified UI presentation, coordinated throughopenlogi-ipcRPC methods
Frequently Asked Questions
What is the difference between Bolt and Unifying discovery in OpenLogi?
Bolt uses active scanning where the receiver continuously searches for devices and reports them via event streams, while Unifying uses a pairing lock that passively waits for devices to enter pairing mode. Bolt's discover_devices writes a scan register; Unifying's open_pairing_lock modifies lock state. Bolt provides richer discovery metadata including device names; Unifying relies on connection events after lock open.
Does OpenLogi support Bluetooth devices without a Bolt receiver?
According to the source code in crates/openlogi-hidpp/src/receiver/bolt.rs, Bluetooth connections require the Bolt receiver acting as a bridge. The discovery pipeline does not implement native Bluetooth LE scanning—Bluetooth devices must pair through a Bolt receiver, which then reports them via the standard Bolt event protocol.
How does OpenLogi avoid duplicate device entries from multiple discovery sources?
The DeviceStore implementation in crates/openlogi-desktop/src/state/device_store.rs deduplicates devices using unique identifiers derived from HID++ registers (WPID, serial number, or USB path). Whether discovered via Bolt scan, Unifying lock event, or wired probe, each device is keyed consistently to prevent duplicate UI entries.
What timeout values are supported for Bolt discovery scans?
The discover_devices function in bolt.rs accepts an optional u8 timeout parameter. Passing None or 0x00 uses the receiver's default 30-second window. Values from 0x01 to 0xFF specify custom durations in seconds, though the actual maximum depends on receiver firmware limits.
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 →