OpenLogi

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

ReceiverVID:PIDTransport
Logi Bolt046D:C548HID++ 1.0 registers (BLE-based)
Unifying046D:C52B, 046D:C532, 046D:C537, 046D:C539HID++ 1.0 registers (Unifying / DJ)
Lightspeed046D:C53F, 046D:C547Unifying 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:

ItemDescription
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:

FunctionPurpose
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

  1. discover_devices(Some(secs)) — the receiver emits Event::DeviceDiscoveryDeviceDetails (carrying the device address, kind, wpid, authentication) and Event::DeviceDiscoveryDeviceName for each nearby device.
  2. pair_device(slot, address, authentication, entropy)entropy sets passkey complexity (for mice, the number of left/right clicks the user must enter).
  3. The receiver completes pairing and emits Event::DeviceConnection.

Events (bolt::Event)

VariantWhen
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:

FunctionPurpose
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:

ByteContent
0Protocol type (eQUAD variant).
1Device-info byte (below).
2–3Wireless PID, little-endian.

The device-info byte packs the device kind and four status flags:

BitsMaskMeaning
0–30x0FDevice kind (0x1 keyboard, 0x2 mouse, …).
40x10Software present — the host declared driver software via the SOFTWARE_PRESENT notification flag (register 0x00).
50x20Link encrypted — the eQUAD radio link uses the encryption key established at pairing.
60x40Link not established — set means the device is offline. Inverted sense.
70x80A 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.

On this page