# How Logi Bolt and Unifying Receiver Pairing Processes Negotiate Passkeys in OpenLogi

> Learn how OpenLogi negotiates Logi Bolt passkeys by writing HID++ 1.0 registers and parsing notifications. Discover the difference with Unifying receivers.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: deep-dive
- Published: 2026-08-26

---

**OpenLogi negotiates Logi Bolt passkeys by writing HID++ 1.0 registers to initiate discovery, parsing 17-byte passkey notifications from the receiver, and translating them into keyboard entry or click sequences depending on the device type, while Unifying receivers follow a simpler lock-only flow without passkey exchange.**

The AprilNEA/OpenLogi repository implements the complete pairing flow for Logitech peripherals by directly manipulating the HID++ 1.0 registers of the receiver. Understanding how the Logi Bolt and Unifying receiver pairing process negotiates passkeys requires examining the two distinct architectural paths: Bolt’s challenge-response authentication versus Unifying’s open-lock pairing model.

## The Two Pairing Architectures

OpenLogi distinguishes between receiver families at the hardware abstraction layer. **Unifying** receivers expose a simple pairing lock that opens and closes without cryptographic challenges, while **Bolt** receivers implement a full passkey negotiation to prevent man-in-the-middle attacks during the discovery phase. This fundamental difference determines which HID++ registers the driver writes and which notification parsers activate during the session.

## Initiating the Pairing Flow

The entry point in [`crates/openlogi-device/src/pairing.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/pairing.rs) branches into protocol-specific discovery phases based on the receiver type detected by the `run_pairing` function.

### Opening the Pairing Lock on Unifying Receivers

For Unifying hardware, the state machine transitions to `PairingPhase::UnifyingPairing` and writes the **UNIFYING_PAIRING** register. This register toggles the receiver’s pairing lock open for a predefined window (typically 30 seconds), allowing any unpaired device in proximity to bind without credential exchange. The flow completes when the receiver assigns a device slot and emits a `PairingEvent::Paired` message containing the slot index.

### Starting Discovery on Bolt Receivers

Bolt devices require explicit discovery initiation. The driver writes the **BOLT_DISCOVERY** register to transition into `PairingPhase::BoltDiscovery`, which activates the receiver’s Bluetooth Low Energy scanning mode. Unlike Unifying, the Bolt receiver does not immediately accept connections; instead, it streams `DeviceFound` HID++ notifications containing discovered device advertisements, requiring user selection before passkey negotiation begins.

## Passkey Negotiation in Logi Bolt

Once a user selects a specific device from the discovery stream, the Bolt receiver initiates the cryptographic handshake that defines the Logi Bolt pairing process.

### Receiving the Passkey Request Notification

After the driver writes the selected device’s BTLE address to the **BOLT_PAIRING** register, the receiver emits a *pairing-passkey-request* notification defined in [`crates/openlogi-hidpp/src/receiver/bolt/event.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/receiver/bolt/event.rs). The raw HID++ payload consists of 17 bytes, where the first 6 bytes contain an ASCII-digit passkey (NUL-padded if the code is shorter than 6 digits). The `passkey(payload)` helper function extracts this string from the byte slice, handling padding truncation automatically.

### Decoding the Passkey Method

The notification parser in [`crates/openlogi-device/src/pairing/notification.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/pairing/notification.rs) decodes the request into the `PasskeyMethod` enum, which branches into two interaction modes:

- **Keyboard**: The device expects the user to type the 6-digit code on the physical keypad of the new keyboard, followed by Enter.
- **Pointer**: The device expects a 10-bit left/right click sequence derived from the passkey digits using the `passkey_to_clicks` function in [`crates/openlogi-core/src/hid/pairing.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/hid/pairing.rs). Each bit maps to a `Click::Left` (0) or `Click::Right` (1) value, transmitted Most Significant Bit first.

### User Interaction and Confirmation

The UI layer in [`crates/openlogi-desktop/src/windows/add_device.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/windows/add_device.rs) renders the `passkey_panel` component based on the `PasskeyMethod` variant. For keyboard devices, the panel displays the 6-digit string with instructions to type it on the new hardware. For pointer devices, the UI renders the left/right click pattern graphically.

As the user enters the passkey, the device transmits **PairingPasskeyPress** events back to the receiver. The Bolt event decoder accumulates these presses until the sequence matches the expected code, at which point the receiver assigns a device slot and emits `PairingEvent::Paired`. If the sequence fails or times out, the state machine emits `PairingEvent::Failed`.

## Implementing Passkey Logic in Code

The following example demonstrates the async event loop for handling pairing sessions across both receiver types:

```rust
use openlogi_device::pairing::{run_pairing, PairingCommand, PairingEvent};

let (mut tx, mut rx) = run_pairing(backend).await?;   // `backend` implements `HidBackend`

while let Some(event) = rx.recv().await {
    match event {
        PairingEvent::Searching => println!("Receiver ready – searching for devices"),
        PairingEvent::DeviceFound(dev) => {
            // Select the device to initiate passkey negotiation (Bolt only)
            tx.send(PairingCommand::Pair(dev)).await?;
        }
        PairingEvent::Passkey(method) => {
            println!("Enter this passkey on the device: {}", method.display());
        }
        PairingEvent::Paired { slot } => {
            println!("Device paired in slot {}", slot);
            break;
        }
        PairingEvent::Failed(err) => {
            eprintln!("Pairing failed: {:?}", err);
            break;
        }
    }
}

```

For pointer devices requiring click sequences, convert the numeric passkey into the binary click pattern:

```rust
use openlogi_core::hid::pairing::Click;

/// Convert a 6‑digit passkey into a MSB‑first click sequence.
fn clicks_from_passkey(passkey: u32) -> Vec<Click> {
    (0..10).rev()
        .map(|bit| if passkey & (1 << bit) != 0 { Click::Right } else { Click::Left })
        .collect()
}

```

## Error Handling and Edge Cases

Malformed passkey notifications trigger `PairingError::MalformedNotification`, which causes the state machine to drop the packet and wait for a valid re-transmission from the receiver. If the user cancels the operation through the UI or the device disconnects before confirmation, the driver sends a cancellation command to close the pairing lock, ensuring the receiver exits discovery mode and releases radio resources.

## Summary

- OpenLogi drives Logi Bolt pairing through direct HID++ 1.0 register writes to **BOLT_DISCOVERY** and **BOLT_PAIRING**, while Unifying receivers use the simpler **UNIFYING_PAIRING** lock register.
- The passkey negotiation relies on 17-byte HID++ notifications parsed in [`crates/openlogi-hidpp/src/receiver/bolt/event.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/receiver/bolt/event.rs), containing 6-byte ASCII passkeys extracted by the `passkey()` helper.
- The `PasskeyMethod` enum in [`crates/openlogi-device/src/pairing/notification.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/pairing/notification.rs) bifurcates the user interaction into Keyboard (digit entry) or Pointer (click sequence) modes.
- Confirmation occurs through a series of **PairingPasskeyPress** events verified against the original challenge, resulting in either `PairingEvent::Paired` with a slot assignment or `PairingEvent::Failed`.

## Frequently Asked Questions

### Does the Unifying receiver use passkeys during pairing?

No. According to the OpenLogi source code, Unifying receivers implement a lock-only pairing model controlled by the **UNIFYING_PAIRING** register. When opened, the receiver accepts the first compatible device that attempts to bind without requiring passkey entry or cryptographic challenge-response.

### How is the 6-digit passkey encoded in the HID++ notification?

The passkey arrives in a 17-byte HID++ notification payload where the first 6 bytes represent ASCII digits (0x30-0x39). Shorter passkeys are NUL-padded (0x00) to fill the 6-byte field. The `passkey()` function in [`crates/openlogi-hidpp/src/receiver/bolt/event.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/receiver/bolt/event.rs) truncates these padding bytes to return a clean string.

### What is the difference between Keyboard and Pointer passkey methods?

The `PasskeyMethod` enum defines **Keyboard** for input devices with alphanumeric keypads, requiring the user to type the 6-digit code directly. **Pointer** applies to mice and trackpads lacking keyboards, where the `passkey_to_clicks` function translates the numeric code into a 10-bit sequence of left (`Click::Left`) and right (`Click::Right`) clicks transmitted MSB-first.

### How does OpenLogi handle incorrect passkey entry?

If the user enters an incorrect passkey, the device continues sending **PairingPasskeyPress** events, but the receiver detects the mismatch and eventually times out the pairing attempt. The state machine in [`crates/openlogi-device/src/pairing.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/pairing.rs) then emits `PairingEvent::Failed`, closing the session and requiring the user to restart discovery by re-writing the **BOLT_DISCOVERY** register.