Skip to main content

hidpp/receiver/
unifying.rs

1//! Implements the Unifying Receiver.
2//!
3//! Unifying is a versatile receiver that can pair up to 6 devices using the
4//! 2.4 GHz eQuad radio protocol. It uses HID++ 1.0 registers for receiver
5//! control; paired devices speak HID++ 2.0 once addressed via their slot index.
6//!
7//! The register layout for device enumeration (`0xB5/0x5N`, `0xB5/0x6N`) is
8//! identical to Bolt's. The device-kind encoding differs from Bolt at values 5+
9//! (see [`DeviceKind`]).
10
11use std::sync::Arc;
12
13use num_enum::{FromPrimitive, IntoPrimitive, TryFromPrimitive};
14
15use crate::{
16    channel::{HidppChannel, MessageListenerGuard},
17    event::EventEmitter,
18    protocol::v10,
19    receiver::{RECEIVER_DEVICE_INDEX, ReceiverError},
20};
21
22/// All USB vendor & product ID pairs that are known to identify Unifying
23/// receivers.
24///
25/// `046d:c539` is the Lightspeed gaming receiver; `046d:c53f` is the Lightspeed
26/// nano receiver (bundled with G-series wireless mice such as the G305). Both
27/// answer the same HID++ 1.0 registers (pairing count, connection state, pairing
28/// information) as Unifying receivers. Callers that surface a user-facing
29/// receiver name label Lightspeed PIDs separately (see `openlogi-hid`).
30/// `0xc53f` was verified against a G305 (paired device wpid `0x4074`).
31pub const VPID_PAIRS: &[(u16, u16)] = &[
32    (0x046d, 0xc52b),
33    (0x046d, 0xc532),
34    (0x046d, 0xc539),
35    (0x046d, 0xc53f),
36];
37
38/// All known registers of the Unifying receiver.
39#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize))]
41#[non_exhaustive]
42#[repr(u8)]
43pub enum Register {
44    /// Controls which notifications the receiver emits. Wireless device-arrival
45    /// (`0x41`) events are only re-broadcast while wireless notifications are
46    /// enabled here; see [`Receiver::set_wireless_notifications`].
47    Notifications = 0x00,
48
49    /// Enables or disables wireless device-connection notifications; also used
50    /// to read the pairing count and to trigger device-arrival events.
51    Connections = 0x02,
52
53    /// Provides information about the receiver and paired devices. It uses
54    /// sub-registers, as defined in [`InfoSubRegister`], to differentiate
55    /// between different kinds of information.
56    ReceiverInfo = 0xb5,
57}
58
59/// Represents the known sub-registers of the [`Register::ReceiverInfo`]
60/// register.
61#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize))]
63#[non_exhaustive]
64#[repr(u8)]
65pub enum InfoSubRegister {
66    /// Provides general information about the receiver (serial number, pairing
67    /// slot count).
68    ReceiverInfo = 0x03,
69
70    /// Provides information about a specific paired device. The device index
71    /// (4 bits) must be added to this base address to form the actual
72    /// sub-register: `0x50 | (device_index & 0x0f)`.
73    DevicePairingInformation = 0x50,
74
75    /// Provides the codename of a specific paired device. The device index (4
76    /// bits) must be added: `0x60 | (device_index & 0x0f)`.
77    ///
78    /// NOTE: `0x60` is the *Bolt* base. Wire-verified Unifying receivers store
79    /// names at base `0x40 + (n-1)` instead, so name reads go directly through
80    /// `read_codename_unifying` in `inventory.rs` rather than this constant —
81    /// don't reuse `DeviceCodename` for Unifying name reads.
82    DeviceCodename = 0x60,
83}
84
85/// Implements the Unifying wireless receiver.
86#[derive(Clone)]
87pub struct Receiver {
88    chan: Arc<HidppChannel>,
89    emitter: Arc<EventEmitter<Event>>,
90    _listener: Arc<MessageListenerGuard>,
91}
92
93impl Receiver {
94    /// Tries to initialize a new [`Receiver`] from a raw HID++ channel.
95    ///
96    /// Returns [`ReceiverError::UnknownReceiver`] when the channel's VID/PID
97    /// doesn't match any known Unifying receiver.
98    pub fn new(chan: Arc<HidppChannel>) -> Result<Self, ReceiverError> {
99        if !VPID_PAIRS.contains(&(chan.vendor_id, chan.product_id)) {
100            return Err(ReceiverError::UnknownReceiver);
101        }
102
103        let emitter = Arc::new(EventEmitter::new());
104
105        let listener = chan.add_msg_listener_guarded({
106            let emitter = Arc::clone(&emitter);
107            move |raw, matched| {
108                if matched {
109                    return;
110                }
111
112                let parsed = v10::Message::from(raw);
113                let header = parsed.header();
114                let payload = parsed.extend_payload();
115
116                // Device-connection notifications are directed at a specific slot
117                // (header.device_index = slot) with sub_id 0x41.
118                if header.sub_id != 0x41 {
119                    return;
120                }
121
122                // Kind is identity-only; an unrecognised nibble folds to
123                // `Unknown` — dropping the event would hide the device
124                // entirely (arrival notifications are the only device
125                // source on this path).
126                emitter.emit(Event::DeviceConnection(DeviceConnection {
127                    index: header.device_index,
128                    kind: DeviceKind::from(payload[1] & 0x0f),
129                    encrypted: payload[1] & (1 << 4) != 0,
130                    online: payload[1] & (1 << 6) == 0,
131                    wpid: u16::from_le_bytes(payload[2..=3].try_into().unwrap()),
132                }));
133            }
134        });
135
136        Ok(Receiver {
137            _listener: Arc::new(listener),
138            chan,
139            emitter,
140        })
141    }
142
143    /// Creates a new listener for receiving receiver events.
144    pub fn listen(&self) -> async_channel::Receiver<Event> {
145        self.emitter.create_receiver()
146    }
147
148    /// Counts the number of devices currently paired to this receiver.
149    /// Offline (sleeping) devices are included since pairings are persistent.
150    pub async fn count_pairings(&self) -> Result<u8, ReceiverError> {
151        let response = self
152            .chan
153            .read_register(
154                RECEIVER_DEVICE_INDEX,
155                Register::Connections.into(),
156                [0u8; 3],
157            )
158            .await?;
159
160        Ok(response[1])
161    }
162
163    /// Enables or disables wireless device-connection notifications.
164    ///
165    /// The receiver only re-broadcasts `0x41` device-arrival events (the source
166    /// for [`Self::trigger_device_arrival`]) while this is on. With it off the
167    /// trigger write is ACK'd but emits nothing — which is why a paired, online
168    /// device can fail to enumerate. Solaar enables this before listing.
169    ///
170    /// Read-modify-write of just the `WIRELESS` bit so it can't clobber other
171    /// flags already set on register `0x00` — notably `SOFTWARE_PRESENT` (0x08),
172    /// which the pairing flow enables (`pairing.rs` writes `[0x00, 0x09, 0x00]`)
173    /// and a concurrent inventory poll would otherwise drop.
174    pub async fn set_wireless_notifications(&self, enabled: bool) -> Result<(), ReceiverError> {
175        // Notification flags are a 3-byte big-endian word; the receiver-reporting
176        // bits live in byte 1 (WIRELESS = 0x000100, SOFTWARE_PRESENT = 0x000800).
177        const WIRELESS: u8 = 0x01;
178        let mut flags = self
179            .chan
180            .read_register(
181                RECEIVER_DEVICE_INDEX,
182                Register::Notifications.into(),
183                [0; 3],
184            )
185            .await?;
186        if enabled {
187            flags[1] |= WIRELESS;
188        } else {
189            flags[1] &= !WIRELESS;
190        }
191        self.chan
192            .write_register(RECEIVER_DEVICE_INDEX, Register::Notifications.into(), flags)
193            .await?;
194
195        Ok(())
196    }
197
198    /// Triggers device-arrival notifications for all currently connected
199    /// devices. Used to enumerate online devices at startup.
200    pub async fn trigger_device_arrival(&self) -> Result<(), ReceiverError> {
201        self.chan
202            .write_register(
203                RECEIVER_DEVICE_INDEX,
204                Register::Connections.into(),
205                [0x02, 0x00, 0x00],
206            )
207            .await?;
208
209        Ok(())
210    }
211
212    /// Provides general information about the receiver (serial number and
213    /// pairing slot count).
214    pub async fn get_receiver_info(&self) -> Result<ReceiverInfo, ReceiverError> {
215        let response = self
216            .chan
217            .read_long_register(
218                RECEIVER_DEVICE_INDEX,
219                Register::ReceiverInfo.into(),
220                [InfoSubRegister::ReceiverInfo.into(), 0, 0],
221            )
222            .await?;
223
224        Ok(ReceiverInfo {
225            serial_number: hex::encode_upper(&response[1..=4]),
226            pairing_slots: response[6],
227        })
228    }
229
230    /// Retrieves the pairing information for the device at `device_index`
231    /// (1-based slot number).
232    pub async fn get_device_pairing_information(
233        &self,
234        device_index: u8,
235    ) -> Result<DevicePairingInformation, ReceiverError> {
236        let response = self
237            .chan
238            .read_long_register(
239                RECEIVER_DEVICE_INDEX,
240                Register::ReceiverInfo.into(),
241                [
242                    u8::from(InfoSubRegister::DevicePairingInformation) | (device_index & 0x0f),
243                    0x00,
244                    0x00,
245                ],
246            )
247            .await?;
248
249        Ok(DevicePairingInformation {
250            wpid: u16::from_le_bytes(response[2..=3].try_into().unwrap()),
251            // Kind is identity-only: an unrecognised nibble folds to
252            // `Unknown` instead of failing the whole pairing-info read.
253            kind: DeviceKind::from(response[1] & 0x0f),
254            encrypted: response[1] & (1 << 4) != 0,
255            online: response[1] & (1 << 6) == 0,
256            unit_id: response[4..=7].try_into().unwrap(),
257        })
258    }
259
260    /// Provides the unique ID of the receiver (serial number).
261    pub async fn get_unique_id(&self) -> Result<String, ReceiverError> {
262        self.get_receiver_info().await.map(|i| i.serial_number)
263    }
264}
265
266/// Represents some general information about a Unifying receiver.
267#[derive(Clone, PartialEq, Eq, Hash, Debug)]
268#[cfg_attr(feature = "serde", derive(serde::Serialize))]
269#[non_exhaustive]
270pub struct ReceiverInfo {
271    /// Receiver serial number.
272    pub serial_number: String,
273    /// Number of available pairing slots.
274    pub pairing_slots: u8,
275}
276
277/// Represents information about a paired device as read from the pairing
278/// register.
279#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
280#[cfg_attr(feature = "serde", derive(serde::Serialize))]
281#[non_exhaustive]
282pub struct DevicePairingInformation {
283    /// Wireless product ID of the paired device.
284    pub wpid: u16,
285    /// Device kind reported by the receiver.
286    pub kind: DeviceKind,
287    /// Whether the link is encrypted.
288    pub encrypted: bool,
289    /// Whether the device is currently online.
290    pub online: bool,
291    /// Device unit ID.
292    pub unit_id: [u8; 4],
293}
294
295/// Represents the kind of a device paired to a Unifying receiver.
296///
297/// The encoding matches Bolt for values 1–4; from 5 onwards Unifying uses a
298/// shifted table (Remote=5, Trackball=6, Touchpad=7) while Bolt reserves those
299/// values and places them at 7–9.
300#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, FromPrimitive)]
301#[cfg_attr(feature = "serde", derive(serde::Serialize))]
302#[non_exhaustive]
303#[repr(u8)]
304pub enum DeviceKind {
305    /// Unknown device kind — also the fold target for values this crate
306    /// does not model (kind is identity-only and must never drop an event).
307    #[num_enum(default)]
308    Unknown = 0x00,
309    /// Keyboard device.
310    Keyboard = 0x01,
311    /// Mouse device.
312    Mouse = 0x02,
313    /// Numeric keypad device.
314    Numpad = 0x03,
315    /// Presenter device.
316    Presenter = 0x04,
317    /// Remote-control device.
318    Remote = 0x05,
319    /// Trackball device.
320    Trackball = 0x06,
321    /// Touchpad device.
322    Touchpad = 0x07,
323}
324
325/// Represents a device-connection event fired by the receiver when a paired
326/// device comes online (or in response to [`Receiver::trigger_device_arrival`]).
327#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
328#[cfg_attr(feature = "serde", derive(serde::Serialize))]
329#[non_exhaustive]
330pub struct DeviceConnection {
331    /// Slot index (1-based) of the device.
332    pub index: u8,
333    /// Device kind reported by the receiver.
334    pub kind: DeviceKind,
335    /// Whether the link is encrypted.
336    pub encrypted: bool,
337    /// Whether the device is currently online.
338    pub online: bool,
339    /// Wireless product ID of the device.
340    pub wpid: u16,
341}
342
343/// Represents an event emitted by the Unifying receiver.
344#[derive(Clone, PartialEq, Eq, Hash, Debug)]
345#[cfg_attr(feature = "serde", derive(serde::Serialize))]
346#[non_exhaustive]
347pub enum Event {
348    /// Fired whenever a paired device connects or reconnects, and for all
349    /// online devices in response to [`Receiver::trigger_device_arrival`].
350    DeviceConnection(DeviceConnection),
351}