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:

  1. Enumerate all accessible HID raw nodes
  2. Read DeviceCodename and DeviceInformation registers
  3. Parse codenames using parse_codename from bolt.rs
  4. Map raw DeviceKind values via map_unifying_kind in inventory/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 DiscoveredDevice structs 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_devices in bolt.rs, parses paired detail/name events using discovery_counter and discovery_name helpers, and completes with pair_device
  • Unifying discovery relies on open_pairing_lock and close_pairing_lock in pairing.rs, with state notifications through UnifyingLock events in notification.rs
  • Wired discovery probes HID nodes via probe_all in probe.rs, leveraging ProbeCache for performance and transport.rs for platform abstraction
  • All paths feed into DeviceStore for unified UI presentation, coordinated through openlogi-ipc RPC 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:

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 →