Receivers
The USB dongles that wireless Logitech devices pair to, driven through HID++ 1.0 registers and identified by USB vendor/product ID.
A receiver is the USB dongle that wireless Logitech devices pair to. OpenLogi detects the receiver on its USB/HID channel, then enumerates the devices paired to it.
Unlike device features (which use HID++ 2.0), receivers are driven through
HID++ 1.0 registers and are always addressed at device index 0xFF
(RECEIVER_DEVICE_INDEX). They are identified purely by their USB vendor/product
ID; openlogi-hidpp's receiver::detect() matches the channel's VID/PID
against the known sets and returns a Receiver.
Implementation status
Receiver support in the vendored hidpp crate is deliberately conservative;
public documentation is scarce. Logi Bolt is the most complete (developed and
tested against real hardware); Unifying covers discovery and enumeration but
its pairing/management surface is thinner and welcomes hardware testing.
Detection
| Receiver | VID:PID | Transport |
|---|---|---|
| Logi Bolt | 046D:C548 | HID++ 1.0 registers (BLE-based) |
| Unifying | 046D:C52B, 046D:C532, 046D:C537, 046D:C539 | HID++ 1.0 registers (Unifying / DJ) |
| Lightspeed | 046D:C53F, 046D:C547 | Unifying registers, G-series dongles |
receiver::detect(chan) returns Some(Receiver) for a known dongle, or None
otherwise.
The Lightspeed dongles bundled with G-series devices answer the same HID++
1.0 registers as Unifying, so they are detected, enumerated, routed, and paired
through the Unifying code path; only the user-facing name differs
(Lightspeed Receiver). C53F is the nano receiver of wireless mice such as the
G305, verified against a G305 (paired device wpid 0x4074); C547 ships with
newer devices such as the G915 keyboard and the G502 X LIGHTSPEED, verified
against a G915 (wpid 0x407c). C539 is the Lightspeed gaming receiver, listed
with the Unifying PIDs because it is routed as one. C537 is the Nano receiver
bundled with the G602; it answers the same enumeration and pairing-information
registers, so it routes as a Unifying receiver.
Common API
The Receiver enum wraps the concrete receiver kinds and exposes the shared
surface:
| Item | Description |
|---|---|
Receiver::Bolt(_) / Receiver::Unifying(_) | The detected receiver kind. |
name() | Human-readable name (e.g. "Logi Bolt Receiver"). |
get_unique_id() | A string that uniquely identifies this receiver (serial or equivalent). |
RECEIVER_DEVICE_INDEX (0xFF) | The device index used to address the receiver on the channel. |
Errors surface as ReceiverError: UnknownReceiver, or a wrapped HID++ 1.0
Protocol error.
Logi Bolt
The current Logitech receiver. Bolt is BLE-based, pairs up to 6 devices,
and authenticates new devices with a passkey before pairing. bolt::Receiver
exposes:
| Function | Purpose |
|---|---|
count_pairings() | Number of currently paired devices (offline ones included). |
collect_paired_devices() | Enumerate all paired devices (Vec<DeviceConnection>). |
get_device_pairing_information(index) | Pairing info for one slot (DevicePairingInformation). |
get_device_codename(index) | The device's codename / name. |
get_notification_state() / set_notification_state(..) | Read / enable receiver notifications. |
trigger_device_arrival() | Re-fire DeviceConnection events for all paired devices (enumeration). |
discover_devices(timeout) / cancel_device_discovery() | Start / stop discovery of pairable devices (≤ 60 s). |
pair_device(slot, address, authentication, entropy) | Begin pairing a discovered device. |
unpair_device(index) | Remove a pairing. |
listen() | Subscribe to receiver Events. |
Pairing flow
discover_devices(Some(secs))— the receiver emitsEvent::DeviceDiscoveryDeviceDetails(carrying the deviceaddress,kind,wpid,authentication) andEvent::DeviceDiscoveryDeviceNamefor each nearby device.pair_device(slot, address, authentication, entropy)—entropysets passkey complexity (for mice, the number of left/right clicks the user must enter).- The receiver completes pairing and emits
Event::DeviceConnection.
Events (bolt::Event)
| Variant | When |
|---|---|
DeviceConnection(DeviceConnection) | A device connects / disconnects (requires wireless notifications enabled). |
DeviceDiscoveryStatus { discovery_enabled } | Discovery mode toggles. |
DeviceDiscoveryDeviceDetails { counter, kind, wpid, address, authentication } | A device is discovered (details + address required to pair). |
DeviceDiscoveryDeviceName { .. } | The discovered device's name. |
DeviceKind covers Keyboard, Mouse, Numpad, Presenter, Remote,
Trackball, Touchpad, Tablet, Gamepad, Joystick, Headset (plus
Unknown).
Unifying
The previous-generation receiver. Unifying pairs up to 6 devices over the
proprietary Unifying / DJ protocol; once addressed by their slot index, paired
devices speak HID++ 2.0. unifying::Receiver exposes:
| Function | Purpose |
|---|---|
count_pairings() | Number of paired devices (offline ones included). |
get_receiver_info() | Receiver info, including pairing_slots (ReceiverInfo). |
get_device_pairing_information(index) | Pairing info for one slot (DevicePairingInformation). |
trigger_device_arrival() | Re-broadcast connection events for every paired slot, offline ones included (startup enumeration). |
get_unique_id() | Receiver serial / unique id. |
listen() | Subscribe to receiver Events. |
The 0x41 connection notification
Both receivers report device state through the same unsolicited HID++ 1.0
notification, sub-id 0x41. It fires when a paired device connects or
disconnects, and trigger_device_arrival() makes the receiver re-broadcast it
for every paired slot — a slot whose device is powered off or out of range
still gets one, marked offline. The notification's device index is the device's
pairing slot, and the payload is:
| Byte | Content |
|---|---|
| 0 | Protocol type (eQUAD variant). |
| 1 | Device-info byte (below). |
| 2–3 | Wireless PID, little-endian. |
The device-info byte packs the device kind and four status flags:
| Bits | Mask | Meaning |
|---|---|---|
| 0–3 | 0x0F | Device kind (0x1 keyboard, 0x2 mouse, …). |
| 4 | 0x10 | Software present — the host declared driver software via the SOFTWARE_PRESENT notification flag (register 0x00). |
| 5 | 0x20 | Link encrypted — the eQUAD radio link uses the encryption key established at pairing. |
| 6 | 0x40 | Link not established — set means the device is offline. Inverted sense. |
| 7 | 0x80 | A payload packet follows. |
Bits 5 and 4 look alike but describe different worlds. Bit 5 is a property of
the 2.4 GHz link itself: whether traffic between device and receiver is
encrypted, decided at pairing time. It matters for security — unencrypted links
can be sniffed and injected over the air (the 2016 MouseJack attacks targeted
exactly these), which is why Solaar warns after pairing when the bit is clear.
Bolt links are always encrypted by design. Bit 4 says nothing about the radio:
it echoes a flag the host wrote into register 0x00 to declare "software is
controlling part of device behaviour", which device firmware uses to decide,
for example, whether gesture buttons emit diverted reports or fall back to
built-in behaviour.
Bit 4 is not encryption
The two bits are easy to conflate: OpenLogi's vendored hidpp fork read bit 4
as the encryption flag until it was checked against Solaar's decoder, and the
layout is identical on Unifying and Bolt — there is no per-receiver variation
to account for.
A captured example, short message payload 04 62 69 40: protocol 0x04
(eQUAD), device-info 0x62 = 0110 0010 — bit 6 set (offline), bit 5 set
(link encrypted at pairing), bit 4 clear, kind 0x2 (mouse) — and wpid
0x4069, an MX Master 2S. That is what an offline slot's re-broadcast looks
like: identity intact, link down.
Enumerating paired devices (Rust)
use std::sync::Arc;
use hidpp::{
channel::HidppChannel,
receiver::{self, Receiver},
};
// chan: Arc<HidppChannel> bound to the receiver's HID interface.
let Some(rx) = receiver::detect(Arc::clone(&chan)) else {
return; // not a known Logitech receiver
};
println!("{} — {}", rx.name(), rx.get_unique_id().await?);
match rx {
Receiver::Bolt(bolt) => {
for dev in bolt.collect_paired_devices().await? {
// dev: bolt::DeviceConnection — one paired device
let _ = dev;
}
}
Receiver::Unifying(uni) => {
let events = uni.listen();
uni.trigger_device_arrival().await?; // re-fires DeviceConnection events
while let Ok(event) = events.recv().await {
// handle unifying::Event::DeviceConnection { .. }
let _ = event;
break;
}
}
_ => {}
}No receiver
Bluetooth-direct and wired devices enumerate as their own inventory; no receiver involved. See Connect a device.