# How OpenLogi Discovers Logitech Devices: Bolt, Unifying, Bluetooth, and Wired Connection Pipelines Explained

> Explore OpenLogi's device discovery pipeline for Logitech Bolt, Unifying, Bluetooth, and wired connections. Understand how OpenLogi connects your devices.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: internals
- Published: 2026-09-05

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/receiver/bolt.rs), which writes the `DeviceDiscovery` register with a timeout value:

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/receiver/bolt/event.rs):

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/pairing.rs):

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/pairing/notification.rs):

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hid/src/probe_cache.rs) eliminates redundant device queries. The inventory logic in [`crates/openlogi-device/src/inventory/probe.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/bolt.rs)
4. Map raw `DeviceKind` values via `map_unifying_kind` in [`inventory/mappings.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/inventory/mappings.rs)

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/pairing.rs), with state notifications through `UnifyingLock` events in [`notification.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/notification.rs)
- **Wired discovery** probes HID nodes via `probe_all` in [`probe.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/probe.rs), leveraging `ProbeCache` for performance and [`transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.