How Logi Bolt and Unifying Receiver Pairing Processes Negotiate Passkeys in OpenLogi
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 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. 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 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_clicksfunction incrates/openlogi-core/src/hid/pairing.rs. Each bit maps to aClick::Left(0) orClick::Right(1) value, transmitted Most Significant Bit first.
User Interaction and Confirmation
The UI layer in 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:
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:
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, containing 6-byte ASCII passkeys extracted by thepasskey()helper. - The
PasskeyMethodenum incrates/openlogi-device/src/pairing/notification.rsbifurcates 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::Pairedwith a slot assignment orPairingEvent::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 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 then emits PairingEvent::Failed, closing the session and requiring the user to restart discovery by re-writing the BOLT_DISCOVERY register.
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 →