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::{IntoPrimitive, TryFromPrimitive};
14
15use crate::{
16    channel::HidppChannel,
17    event::EventEmitter,
18    protocol::v10::{self, Hidpp10Error},
19    receiver::{ListenerDropGuard, 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    /// Enables or disables wireless device-connection notifications; also used
33    /// to read the pairing count and to trigger device-arrival events.
34    Connections = 0x02,
35
36    /// Provides information about the receiver and paired devices. It uses
37    /// sub-registers, as defined in [`InfoSubRegister`], to differentiate
38    /// between different kinds of information.
39    ReceiverInfo = 0xb5,
40}
41
42/// Represents the known sub-registers of the [`Register::ReceiverInfo`]
43/// register.
44#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize))]
46#[non_exhaustive]
47#[repr(u8)]
48pub enum InfoSubRegister {
49    /// Provides general information about the receiver (serial number, pairing
50    /// slot count).
51    ReceiverInfo = 0x03,
52
53    /// Provides information about a specific paired device. The device index
54    /// (4 bits) must be added to this base address to form the actual
55    /// sub-register: `0x50 | (device_index & 0x0f)`.
56    DevicePairingInformation = 0x50,
57
58    /// Provides the codename of a specific paired device. The device index (4
59    /// bits) must be added: `0x60 | (device_index & 0x0f)`.
60    DeviceCodename = 0x60,
61}
62
63/// Implements the Unifying wireless receiver.
64#[derive(Clone)]
65pub struct Receiver {
66    chan: Arc<HidppChannel>,
67    emitter: Arc<EventEmitter<Event>>,
68    _listener: Arc<ListenerDropGuard>,
69}
70
71impl Receiver {
72    /// Tries to initialize a new [`Receiver`] from a raw HID++ channel.
73    ///
74    /// Returns [`ReceiverError::UnknownReceiver`] when the channel's VID/PID
75    /// doesn't match any known Unifying receiver.
76    pub fn new(chan: Arc<HidppChannel>) -> Result<Self, ReceiverError> {
77        if !VPID_PAIRS.contains(&(chan.vendor_id, chan.product_id)) {
78            return Err(ReceiverError::UnknownReceiver);
79        }
80
81        let emitter = Arc::new(EventEmitter::new());
82
83        let hdl = chan.add_msg_listener({
84            let emitter = Arc::clone(&emitter);
85            move |raw, matched| {
86                if matched {
87                    return;
88                }
89
90                let parsed = v10::Message::from(raw);
91                let header = parsed.header();
92                let payload = parsed.extend_payload();
93
94                // Device-connection notifications are directed at a specific slot
95                // (header.device_index = slot) with sub_id 0x41.
96                if header.sub_id != 0x41 {
97                    return;
98                }
99
100                let Ok(kind) = DeviceKind::try_from(payload[1] & 0x0f) else {
101                    return;
102                };
103
104                emitter.emit(Event::DeviceConnection(DeviceConnection {
105                    index: header.device_index,
106                    kind,
107                    encrypted: payload[1] & (1 << 4) != 0,
108                    online: payload[1] & (1 << 6) == 0,
109                    wpid: u16::from_le_bytes(payload[2..=3].try_into().unwrap()),
110                }));
111            }
112        });
113
114        Ok(Receiver {
115            _listener: Arc::new(ListenerDropGuard {
116                chan: Arc::clone(&chan),
117                hdl,
118            }),
119            chan,
120            emitter,
121        })
122    }
123
124    /// Creates a new listener for receiving receiver events.
125    pub fn listen(&self) -> async_channel::Receiver<Event> {
126        self.emitter.create_receiver()
127    }
128
129    /// Counts the number of devices currently paired to this receiver.
130    /// Offline (sleeping) devices are included since pairings are persistent.
131    pub async fn count_pairings(&self) -> Result<u8, ReceiverError> {
132        let response = self
133            .chan
134            .read_register(
135                RECEIVER_DEVICE_INDEX,
136                Register::Connections.into(),
137                [0u8; 3],
138            )
139            .await?;
140
141        Ok(response[1])
142    }
143
144    /// Triggers device-arrival notifications for all currently connected
145    /// devices. Used to enumerate online devices at startup.
146    pub async fn trigger_device_arrival(&self) -> Result<(), ReceiverError> {
147        self.chan
148            .write_register(
149                RECEIVER_DEVICE_INDEX,
150                Register::Connections.into(),
151                [0x02, 0x00, 0x00],
152            )
153            .await?;
154
155        Ok(())
156    }
157
158    /// Provides general information about the receiver (serial number and
159    /// pairing slot count).
160    pub async fn get_receiver_info(&self) -> Result<ReceiverInfo, ReceiverError> {
161        let response = self
162            .chan
163            .read_long_register(
164                RECEIVER_DEVICE_INDEX,
165                Register::ReceiverInfo.into(),
166                [InfoSubRegister::ReceiverInfo.into(), 0, 0],
167            )
168            .await?;
169
170        Ok(ReceiverInfo {
171            serial_number: hex::encode_upper(&response[1..=4]),
172            pairing_slots: response[6],
173        })
174    }
175
176    /// Retrieves the pairing information for the device at `device_index`
177    /// (1-based slot number).
178    pub async fn get_device_pairing_information(
179        &self,
180        device_index: u8,
181    ) -> Result<DevicePairingInformation, ReceiverError> {
182        let response = self
183            .chan
184            .read_long_register(
185                RECEIVER_DEVICE_INDEX,
186                Register::ReceiverInfo.into(),
187                [
188                    u8::from(InfoSubRegister::DevicePairingInformation) | (device_index & 0x0f),
189                    0x00,
190                    0x00,
191                ],
192            )
193            .await?;
194
195        Ok(DevicePairingInformation {
196            wpid: u16::from_le_bytes(response[2..=3].try_into().unwrap()),
197            kind: DeviceKind::try_from(response[1] & 0x0f)
198                .map_err(|_| Hidpp10Error::UnsupportedResponse)?,
199            encrypted: response[1] & (1 << 4) != 0,
200            online: response[1] & (1 << 6) == 0,
201            unit_id: response[4..=7].try_into().unwrap(),
202        })
203    }
204
205    /// Provides the unique ID of the receiver (serial number).
206    pub async fn get_unique_id(&self) -> Result<String, ReceiverError> {
207        self.get_receiver_info().await.map(|i| i.serial_number)
208    }
209}
210
211/// Represents some general information about a Unifying receiver.
212#[derive(Clone, PartialEq, Eq, Hash, Debug)]
213#[cfg_attr(feature = "serde", derive(serde::Serialize))]
214#[non_exhaustive]
215pub struct ReceiverInfo {
216    pub serial_number: String,
217    pub pairing_slots: u8,
218}
219
220/// Represents information about a paired device as read from the pairing
221/// register.
222#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
223#[cfg_attr(feature = "serde", derive(serde::Serialize))]
224#[non_exhaustive]
225pub struct DevicePairingInformation {
226    pub wpid: u16,
227    pub kind: DeviceKind,
228    pub encrypted: bool,
229    pub online: bool,
230    pub unit_id: [u8; 4],
231}
232
233/// Represents the kind of a device paired to a Unifying receiver.
234///
235/// The encoding matches Bolt for values 1–4; from 5 onwards Unifying uses a
236/// shifted table (Remote=5, Trackball=6, Touchpad=7) while Bolt reserves those
237/// values and places them at 7–9.
238#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
239#[cfg_attr(feature = "serde", derive(serde::Serialize))]
240#[non_exhaustive]
241#[repr(u8)]
242pub enum DeviceKind {
243    Unknown = 0x00,
244    Keyboard = 0x01,
245    Mouse = 0x02,
246    Numpad = 0x03,
247    Presenter = 0x04,
248    Remote = 0x05,
249    Trackball = 0x06,
250    Touchpad = 0x07,
251}
252
253/// Represents a device-connection event fired by the receiver when a paired
254/// device comes online (or in response to [`Receiver::trigger_device_arrival`]).
255#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
256#[cfg_attr(feature = "serde", derive(serde::Serialize))]
257#[non_exhaustive]
258pub struct DeviceConnection {
259    /// Slot index (1-based) of the device.
260    pub index: u8,
261    pub kind: DeviceKind,
262    pub encrypted: bool,
263    pub online: bool,
264    /// Wireless product ID of the device.
265    pub wpid: u16,
266}
267
268/// Represents an event emitted by the Unifying receiver.
269#[derive(Clone, PartialEq, Eq, Hash, Debug)]
270#[cfg_attr(feature = "serde", derive(serde::Serialize))]
271#[non_exhaustive]
272pub enum Event {
273    /// Fired whenever a paired device connects or reconnects, and for all
274    /// online devices in response to [`Receiver::trigger_device_arrival`].
275    DeviceConnection(DeviceConnection),
276}