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