Skip to main content

hidpp/receiver/
bolt.rs

1//! Implements the Logi Bolt receiver.
2//!
3//! Bolt can be seen as a successor to the Unifying receiver. Both of them
4//! support up to 6 paired devices, but Bolt uses BTLE technology and introduces
5//! so-called passkeys for authenticating devices before pairing them.
6//!
7//! There is little to no public documentation about what registers Bolt
8//! supports (and they seem to differ quite substantially from registers
9//! supported by Unifying and other receivers), so this implementation is based
10//! largely on information gathered by looking at other codebases (primarily
11//! Solaar) and searching registers by fuzzing them.
12
13use std::sync::Arc;
14
15use derive_builder::Builder;
16use futures::{FutureExt, pin_mut, select};
17use num_enum::{IntoPrimitive, TryFromPrimitive};
18
19use super::{RECEIVER_DEVICE_INDEX, ReceiverError};
20use crate::{
21    channel::{HidppChannel, MessageListenerGuard},
22    event::EventEmitter,
23    protocol::v10::{self, Hidpp10Error},
24};
25
26mod event;
27
28pub use event::{DeviceConnection, DeviceKind, Event, PairingError, PairingPasskeyPressType};
29
30/// All USB vendor & product ID pairs that are known to identify Bolt receivers.
31pub const VPID_PAIRS: &[(u16, u16)] = &[(0x046d, 0xc548)];
32
33/// All known registers of the Bolt receiver.
34///
35/// In most cases you should not need to access these manually, as [`Receiver`]
36/// implements many features.
37#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize))]
39#[non_exhaustive]
40#[repr(u8)]
41pub enum Register {
42    /// Allows control over what notifications the receiver sends.
43    Notifications = 0x00,
44
45    /// Provides the amount of currently paired devices.
46    ///
47    /// This is exposed by [`Receiver::count_pairings`].
48    Connections = 0x02,
49
50    /// Provides information about the receiver and paired devices.
51    ///
52    /// It uses sub-registers, as defined in [`InfoSubRegister`], to
53    /// differentiate between different kinds of information.
54    ReceiverInfo = 0xb5,
55
56    /// Provides support for discovering devices that are ready to pair.
57    ///
58    /// Use [`Receiver::discover_devices`] and
59    /// [`Receiver::cancel_device_discovery`] to control device discovery.
60    DeviceDiscovery = 0xc0,
61
62    /// Provides pairing and unpairing support.
63    ///
64    /// Use [`Receiver::pair_device`] and [`Receiver::unpair_device`] for
65    /// pairing and unpairing.
66    Pairing = 0xc1,
67
68    /// Exposes the unique ID of the receiver. This seems to differ from the
69    /// serial number.
70    ///
71    /// Use [`Receiver::get_unique_id`] to query this value.
72    UniqueId = 0xfb,
73}
74
75/// All known sub-registers of the [`Register::ReceiverInfo`] register.
76#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
77#[cfg_attr(feature = "serde", derive(serde::Serialize))]
78#[non_exhaustive]
79#[repr(u8)]
80pub enum InfoSubRegister {
81    /// Provides information about a specific paired device. The device index (4
82    /// bits) has to be added to the register address.
83    ///
84    /// Exposed by [`Receiver::get_device_pairing_information`].
85    DevicePairingInformation = 0x50, // 0x5N with N = device index
86
87    /// Provides the name of a paired device. The device index (4
88    /// bits) has to be added to the register address.
89    ///
90    /// Exposed by [`Receiver::get_device_codename`].
91    DeviceCodename = 0x60, // 0x6N with N = device index
92}
93
94/// Implements the Bolt receiver.
95#[derive(Clone)]
96pub struct Receiver {
97    chan: Arc<HidppChannel>,
98    emitter: Arc<EventEmitter<Event>>,
99    _listener: Arc<MessageListenerGuard>,
100}
101
102impl Receiver {
103    /// Tries to initialize a new [`Receiver`] from a raw HID++ channel.
104    ///
105    /// If no receiver could be found, or if the vendor and product IDs don't
106    /// match the ones of any known Bolt receiver, this function will return
107    /// [`ReceiverError::UnknownReceiver`].
108    pub fn new(chan: Arc<HidppChannel>) -> Result<Self, ReceiverError> {
109        if !VPID_PAIRS.contains(&(chan.vendor_id, chan.product_id)) {
110            return Err(ReceiverError::UnknownReceiver);
111        }
112
113        let emitter = Arc::new(EventEmitter::new());
114
115        let listener = chan.add_msg_listener_guarded({
116            let emitter = Arc::clone(&emitter);
117
118            move |raw, matched| {
119                // A report already matched to an outgoing request is a
120                // response, not a notification.
121                if matched {
122                    return;
123                }
124
125                if let Some(event) = event::decode(&v10::Message::from(raw)) {
126                    emitter.emit(event);
127                }
128            }
129        });
130
131        Ok(Receiver {
132            _listener: Arc::new(listener),
133            chan,
134            emitter,
135        })
136    }
137
138    /// Creates a new listener for receiving receiver events.
139    #[must_use]
140    pub fn listen(&self) -> async_channel::Receiver<Event> {
141        self.emitter.create_receiver()
142    }
143
144    /// Queries the current information about what notifications are enabled.
145    pub async fn get_notification_state(&self) -> Result<NotificationState, ReceiverError> {
146        let response = self
147            .chan
148            .read_register(
149                RECEIVER_DEVICE_INDEX,
150                Register::Notifications.into(),
151                [0u8; 3],
152            )
153            .await?;
154
155        Ok(NotificationState {
156            wireless_notifications: (response[1] & 1) != 0,
157        })
158    }
159
160    /// Configures what notifications are enabled and thus reported by the
161    /// receiver.
162    pub async fn set_notification_state(
163        &self,
164        state: NotificationState,
165    ) -> Result<(), ReceiverError> {
166        self.chan
167            .write_register(
168                RECEIVER_DEVICE_INDEX,
169                Register::Notifications.into(),
170                [0, u8::from(state.wireless_notifications), 0],
171            )
172            .await?;
173
174        Ok(())
175    }
176
177    /// Counts the amount of devices currently paired to this receiver. The
178    /// devices don't have to be online to be included here as pairings are
179    /// persistent.
180    pub async fn count_pairings(&self) -> Result<u8, ReceiverError> {
181        let response = self
182            .chan
183            .read_register(
184                RECEIVER_DEVICE_INDEX,
185                Register::Connections.into(),
186                [0u8; 3],
187            )
188            .await?;
189
190        Ok(response[1])
191    }
192
193    /// Triggers device arrival notifications for all devices currently
194    /// connected to the receiver. This is useful for device enumeration.
195    ///
196    /// Check [`Self::get_notification_state`] first to make sure that
197    /// [`NotificationState::wireless_notifications`] is enabled.
198    pub async fn trigger_device_arrival(&self) -> Result<(), ReceiverError> {
199        self.chan
200            .write_register(
201                RECEIVER_DEVICE_INDEX,
202                Register::Connections.into(),
203                [0x02, 0x00, 0x00],
204            )
205            .await?;
206
207        Ok(())
208    }
209
210    /// Collects information about all paired devices by calling
211    /// [`Self::trigger_device_arrival`] and collecting incoming
212    /// [`Event::DeviceConnection`] events.
213    ///
214    /// Check [`Self::get_notification_state`] first to make sure that
215    /// [`NotificationState::wireless_notifications`] is enabled.
216    pub async fn collect_paired_devices(&self) -> Result<Vec<DeviceConnection>, ReceiverError> {
217        // The idea here is that, when triggering fake device arrival notifications, the
218        // receiver will send the register write confirmation message only AFTER sending
219        // all arrival notifications.
220        // So we will trigger device arrival notifications and continue collecting those
221        // until the original future has completed.
222
223        let mut devices = vec![];
224
225        let rx = self.listen();
226        let fin = self.trigger_device_arrival().fuse();
227        pin_mut!(fin);
228
229        loop {
230            select! {
231                _ = fin => break,
232                res = rx.recv().fuse() => {
233                    let Ok(Event::DeviceConnection(connection)) = res else {
234                        continue;
235                    };
236
237                    devices.push(connection);
238                }
239            }
240        }
241
242        Ok(devices)
243    }
244
245    /// Retrieves the unique ID of the receiver. This is not the same as the
246    /// serial number.
247    pub async fn get_unique_id(&self) -> Result<String, ReceiverError> {
248        let response = self
249            .chan
250            .read_long_register(RECEIVER_DEVICE_INDEX, Register::UniqueId.into(), [0u8; 3])
251            .await?;
252
253        // When decoding the last 8 bytes of the response to their ASCII representation
254        // we seem to get a valid hex string representing 4 bytes of data.
255        // Interpreting this hex string as little endian we seem to get the same decimal
256        // value the Options+ software calls `udid` (unique device identifier?). I am
257        // not sure what this is about and it may be a (major) coincidence that these
258        // values match for my receiver, but it could be worth keeping this in mind.
259
260        // I have no clue how to retrieve the serial number of the receiver.
261
262        Ok(str::from_utf8(&response)
263            .map_err(|_| Hidpp10Error::UnsupportedResponse)?
264            .to_string())
265    }
266
267    /// Provides the pairing information of a specific paired device by its
268    /// index.
269    pub async fn get_device_pairing_information(
270        &self,
271        device_index: u8,
272    ) -> Result<DevicePairingInformation, ReceiverError> {
273        let response = self
274            .chan
275            .read_long_register(
276                RECEIVER_DEVICE_INDEX,
277                Register::ReceiverInfo.into(),
278                [
279                    u8::from(InfoSubRegister::DevicePairingInformation) + (device_index & 0x0f),
280                    0x00,
281                    0x00,
282                ],
283            )
284            .await?;
285
286        Ok(DevicePairingInformation {
287            wpid: u16::from_le_bytes([response[2], response[3]]),
288            // Kind is identity-only: an unrecognised nibble folds to
289            // `Unknown` instead of failing the whole pairing-info read.
290            kind: DeviceKind::from(response[1] & 0x0f),
291            encrypted: response[1] & (1 << 5) != 0,
292            online: response[1] & (1 << 6) == 0,
293            unit_id: [response[4], response[5], response[6], response[7]],
294        })
295    }
296
297    /// Provides the codename of a specific paired device by its index.
298    pub async fn get_device_codename(&self, device_index: u8) -> Result<String, ReceiverError> {
299        // For device names longer than 13 characters this may need to be called
300        // multiple times with different parameters. I don't have a device with
301        // such a name to be able to test this.
302
303        let response = self
304            .chan
305            .read_long_register(
306                RECEIVER_DEVICE_INDEX,
307                Register::ReceiverInfo.into(),
308                [
309                    u8::from(InfoSubRegister::DeviceCodename) + (device_index & 0x0f),
310                    0x01,
311                    0x00,
312                ],
313            )
314            .await?;
315
316        Ok(parse_codename(&response)
317            .ok_or(Hidpp10Error::UnsupportedResponse)?
318            .to_string())
319    }
320
321    /// Unpairs a device from the receiver by its index.
322    pub async fn unpair_device(&self, device_index: u8) -> Result<(), ReceiverError> {
323        let mut payload = [0u8; 16];
324        payload[0] = 0x03;
325        payload[1] = device_index;
326
327        self.chan
328            .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
329            .await?;
330
331        Ok(())
332    }
333
334    /// Starts the pairing process for a new device.
335    ///
336    /// The required `address` and `authentication` values are usually
337    /// discovered from the [`Event::DeviceDiscoveryDeviceDetails`] event which
338    /// is emitted regularly when actively discovering available devices
339    /// ([`Self::discover_devices`]).
340    ///
341    /// `entropy` specifies how complex the authentication passkey should be.
342    /// For mice, this defines the amount of keypresses (left or right) the user
343    /// has to perform. Not all values seem to be supported.
344    pub async fn pair_device(
345        &self,
346        slot: u8,
347        address: [u8; 6],
348        authentication: u8,
349        entropy: u8,
350    ) -> Result<(), ReceiverError> {
351        let mut payload = [0u8; 16];
352        payload[0] = 0x01;
353        payload[1] = slot;
354        payload[2..=7].copy_from_slice(&address);
355        payload[8] = authentication;
356        payload[9] = entropy;
357
358        self.chan
359            .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
360            .await?;
361
362        Ok(())
363    }
364
365    /// Starts device discovery for `timeout` seconds ([`None`] = default, seems
366    /// to be 30s). The maximum supported value is 60s.
367    ///
368    /// While device discovery is enabled,
369    /// [`Event::DeviceDiscoveryDeviceDetails`] and
370    /// [`Event::DeviceDiscoveryDeviceName`] events are emitted for every
371    /// discovered device.
372    pub async fn discover_devices(&self, timeout: Option<u8>) -> Result<(), ReceiverError> {
373        self.chan
374            .write_register(
375                RECEIVER_DEVICE_INDEX,
376                Register::DeviceDiscovery.into(),
377                [timeout.unwrap_or(0x00), 0x01, 0x00],
378            )
379            .await?;
380
381        Ok(())
382    }
383
384    /// Cancels the device discovery process.
385    pub async fn cancel_device_discovery(&self) -> Result<(), ReceiverError> {
386        self.chan
387            .write_register(
388                RECEIVER_DEVICE_INDEX,
389                Register::DeviceDiscovery.into(),
390                [0x00, 0x02, 0x00],
391            )
392            .await?;
393
394        Ok(())
395    }
396}
397
398/// Extract the codename chunk from a `DeviceCodename` register read.
399///
400/// `response[2]` is the device-reported name length. A name longer than the
401/// 13 bytes one response carries is clamped to the chunk present (fetching
402/// the rest takes further reads with different parameters); a length byte
403/// pointing past the response must not panic. `None` for non-UTF-8 bytes.
404fn parse_codename(response: &[u8; 16]) -> Option<&str> {
405    let end = 3usize.saturating_add(usize::from(response[2]));
406    let raw = response.get(3..end.min(response.len()))?;
407    str::from_utf8(raw).ok()
408}
409
410/// Indicates which notifications are enabled and thus sent by the receiver.
411///
412/// This information can be queried using [`Receiver::get_notification_state`].
413#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Builder)]
414#[cfg_attr(feature = "serde", derive(serde::Serialize))]
415#[non_exhaustive]
416pub struct NotificationState {
417    /// Whether the receiver sends device arrival/removal notifications.
418    pub wireless_notifications: bool,
419}
420
421/// Represents information about a paired device.
422///
423/// This information can be queried using
424/// [`Receiver::get_device_pairing_information`].
425#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
426#[cfg_attr(feature = "serde", derive(serde::Serialize))]
427#[non_exhaustive]
428pub struct DevicePairingInformation {
429    /// Wireless product ID of the paired device.
430    pub wpid: u16,
431    /// Device kind reported by the receiver.
432    pub kind: DeviceKind,
433    /// Whether the link is encrypted.
434    pub encrypted: bool,
435    /// Whether the device is currently online.
436    pub online: bool,
437    /// Device unit ID.
438    pub unit_id: [u8; 4],
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444
445    #[test]
446    fn codename_with_oversized_length_clamps_to_available_chunk() {
447        let mut response = [0u8; 16];
448        response[2] = 200;
449        response[3..16].copy_from_slice(b"MX Anywhere 3");
450
451        assert_eq!(parse_codename(&response), Some("MX Anywhere 3"));
452    }
453
454    #[test]
455    fn codename_within_bounds_parses() {
456        let mut response = [0u8; 16];
457        response[2] = 5;
458        response[3..8].copy_from_slice(b"Casa!");
459
460        assert_eq!(parse_codename(&response), Some("Casa!"));
461    }
462
463    #[test]
464    fn codename_rejects_invalid_utf8() {
465        let mut response = [0u8; 16];
466        response[2] = 2;
467        response[3] = 0xff;
468        response[4] = 0xfe;
469
470        assert_eq!(parse_codename(&response), None);
471    }
472}