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,
22    event::EventEmitter,
23    protocol::v10::{self, Hidpp10Error},
24};
25
26/// All USB vendor & product ID pairs that are known to identify Bolt receivers.
27pub const VPID_PAIRS: &[(u16, u16)] = &[(0x046d, 0xc548)];
28
29/// All known registers of the Bolt receiver.
30///
31/// In most cases you should not need to access these manually, as [`Receiver`]
32/// implements many features.
33#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
34#[cfg_attr(feature = "serde", derive(serde::Serialize))]
35#[non_exhaustive]
36#[repr(u8)]
37pub enum Register {
38    /// Allows control over what notifications the receiver sends.
39    Notifications = 0x00,
40
41    /// Provides the amount of currently paired devices.
42    ///
43    /// This is exposed by [`Receiver::count_pairings`].
44    Connections = 0x02,
45
46    /// Provides information about the receiver and paired devices.
47    ///
48    /// It uses sub-registers, as defined in [`InfoSubRegister`], to
49    /// differentiate between different kinds of information.
50    ReceiverInfo = 0xb5,
51
52    /// Provides support for discovering devices that are ready to pair.
53    ///
54    /// Use [`Receiver::discover_devices`] and
55    /// [`Receiver::cancel_device_discovery`] to control device discovery.
56    DeviceDiscovery = 0xc0,
57
58    /// Provides pairing and unpairing support.
59    ///
60    /// Use [`Receiver::pair_device`] and [`Receiver::unpair_device`] for
61    /// pairing and unpairing.
62    Pairing = 0xc1,
63
64    /// Exposes the unique ID of the receiver. This seems to differ from the
65    /// serial number.
66    ///
67    /// Use [`Receiver::get_unique_id`] to query this value.
68    UniqueId = 0xfb,
69}
70
71/// All known sub-registers of the [`Register::ReceiverInfo`] register.
72#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
73#[cfg_attr(feature = "serde", derive(serde::Serialize))]
74#[non_exhaustive]
75#[repr(u8)]
76pub enum InfoSubRegister {
77    /// Provides information about a specific paired device. The device index (4
78    /// bits) has to be added to the register address.
79    ///
80    /// Exposed by [`Receiver::get_device_pairing_information`].
81    DevicePairingInformation = 0x50, // 0x5N with N = device index
82
83    /// Provides the name of a paired device. The device index (4
84    /// bits) has to be added to the register address.
85    ///
86    /// Exposed by [`Receiver::get_device_codename`].
87    DeviceCodename = 0x60, // 0x6N with N = device index
88}
89
90/// Implements the Bolt receiver.
91#[derive(Clone)]
92pub struct Receiver {
93    chan: Arc<HidppChannel>,
94    emitter: Arc<EventEmitter<Event>>,
95    msg_listener_hdl: u32,
96}
97
98impl Receiver {
99    /// Tries to initialize a new [`Receiver`] from a raw HID++ channel.
100    ///
101    /// If no receiver could be found, or if the vendor and product IDs don't
102    /// match the ones of any known Bolt receiver, this function will return
103    /// [`ReceiverError::UnknownReceiver`].
104    pub fn new(chan: Arc<HidppChannel>) -> Result<Self, ReceiverError> {
105        if !VPID_PAIRS.contains(&(chan.vendor_id, chan.product_id)) {
106            return Err(ReceiverError::UnknownReceiver);
107        }
108
109        let emitter = Arc::new(EventEmitter::new());
110
111        let hdl = chan.add_msg_listener({
112            let emitter = Arc::clone(&emitter);
113
114            move |raw, matched| {
115                if matched {
116                    return;
117                }
118
119                let parsed = v10::Message::from(raw);
120                let header = parsed.header();
121                let payload = parsed.extend_payload();
122
123                if header.device_index != RECEIVER_DEVICE_INDEX && header.sub_id != 0x41 {
124                    return;
125                }
126
127                match header.sub_id {
128                    // Device connection
129                    0x41 => {
130                        let Ok(kind) = DeviceKind::try_from(payload[1] & 0x0f) else {
131                            return;
132                        };
133
134                        emitter.emit(Event::DeviceConnection(DeviceConnection {
135                            index: header.device_index,
136                            kind,
137                            encrypted: payload[1] & (1 << 5) != 0,
138                            online: payload[1] & (1 << 6) == 0,
139                            wpid: u16::from_le_bytes(payload[2..=3].try_into().unwrap()),
140                        }));
141                    }
142                    // Device discovery
143                    0x4f => {
144                        match payload[2] {
145                            // Device data
146                            0 => {
147                                let Ok(kind) = DeviceKind::try_from(payload[4] & 0x0f) else {
148                                    return;
149                                };
150
151                                emitter.emit(Event::DeviceDiscoveryDeviceDetails {
152                                    counter: payload[0] as u16 + payload[1] as u16 * 256,
153                                    kind,
154                                    wpid: u16::from_le_bytes(payload[5..=6].try_into().unwrap()),
155                                    address: payload[7..=12].try_into().unwrap(),
156                                    authentication: payload[15],
157                                });
158                            }
159                            // Device name
160                            1 => {
161                                let Ok(name) =
162                                    str::from_utf8(&payload[4..(4 + payload[3] as usize)])
163                                else {
164                                    return;
165                                };
166
167                                emitter.emit(Event::DeviceDiscoveryDeviceName {
168                                    counter: payload[0] as u16 + payload[1] as u16 * 256,
169                                    name: name.to_string(),
170                                });
171                            }
172                            _ => (),
173                        }
174                    }
175                    // Device discovery status
176                    0x53 => {
177                        emitter.emit(Event::DeviceDiscoveryStatus {
178                            discovery_enabled: payload[0] == 0x00,
179                        });
180                    }
181                    // Pairing status
182                    0x54 => {
183                        // payload[0] contains some kind of information about the status. I don't
184                        // know how to map that though.
185
186                        let error = if payload[1] == 0x00 {
187                            None
188                        } else {
189                            let Ok(parsed) = PairingError::try_from(payload[1]) else {
190                                return;
191                            };
192
193                            Some(parsed)
194                        };
195
196                        emitter.emit(Event::PairingStatus {
197                            device_address: payload[2..=7].try_into().unwrap(),
198                            pairing_error: error,
199                            slot: if payload[8] == 0x00 {
200                                None
201                            } else {
202                                Some(payload[8])
203                            },
204                        });
205                    }
206                    // Passkey request
207                    0x4d => {
208                        let Ok(passkey) = str::from_utf8(&payload[1..=6]) else {
209                            return;
210                        };
211
212                        emitter.emit(Event::PairingPasskeyRequest {
213                            device_address: payload[7..=12].try_into().unwrap(),
214                            passkey: passkey.to_string(),
215                        });
216                    }
217                    // Passkey pressed
218                    0x4e => {
219                        let Ok(press_type) = PairingPasskeyPressType::try_from(payload[0]) else {
220                            return;
221                        };
222
223                        emitter.emit(Event::PairingPasskeyPressed {
224                            device_address: payload[1..=6].try_into().unwrap(),
225                            press_type,
226                        });
227                    }
228                    _ => (),
229                }
230            }
231        });
232
233        Ok(Receiver {
234            chan,
235            emitter,
236            msg_listener_hdl: hdl,
237        })
238    }
239
240    /// Creates a new listener for receiving receiver events.
241    pub fn listen(&self) -> async_channel::Receiver<Event> {
242        self.emitter.create_receiver()
243    }
244
245    /// Queries the current information about what notifications are enabled.
246    pub async fn get_notification_state(&self) -> Result<NotificationState, ReceiverError> {
247        let response = self
248            .chan
249            .read_register(
250                RECEIVER_DEVICE_INDEX,
251                Register::Notifications.into(),
252                [0u8; 3],
253            )
254            .await?;
255
256        Ok(NotificationState {
257            wireless_notifications: (response[1] & 1) != 0,
258        })
259    }
260
261    /// Configures what notifications are enabled and thus reported by the
262    /// receiver.
263    pub async fn set_notification_state(
264        &self,
265        state: NotificationState,
266    ) -> Result<(), ReceiverError> {
267        self.chan
268            .write_register(
269                RECEIVER_DEVICE_INDEX,
270                Register::Notifications.into(),
271                [0, if state.wireless_notifications { 1 } else { 0 }, 0],
272            )
273            .await?;
274
275        Ok(())
276    }
277
278    /// Counts the amount of devices currently paired to this receiver. The
279    /// devices don't have to be online to be included here as pairings are
280    /// persistent.
281    pub async fn count_pairings(&self) -> Result<u8, ReceiverError> {
282        let response = self
283            .chan
284            .read_register(
285                RECEIVER_DEVICE_INDEX,
286                Register::Connections.into(),
287                [0u8; 3],
288            )
289            .await?;
290
291        Ok(response[1])
292    }
293
294    /// Triggers device arrival notifications for all devices currently
295    /// connected to the receiver. This is useful for device enumeration.
296    ///
297    /// Check [`Self::get_notification_state`] first to make sure that
298    /// [`NotificationState::wireless_notifications`] is enabled.
299    pub async fn trigger_device_arrival(&self) -> Result<(), ReceiverError> {
300        self.chan
301            .write_register(
302                RECEIVER_DEVICE_INDEX,
303                Register::Connections.into(),
304                [0x02, 0x00, 0x00],
305            )
306            .await?;
307
308        Ok(())
309    }
310
311    /// Collects information about all paired devices by calling
312    /// [`Self::trigger_device_arrival`] and collecting incoming
313    /// [`Event::DeviceConnection`] events.
314    ///
315    /// Check [`Self::get_notification_state`] first to make sure that
316    /// [`NotificationState::wireless_notifications`] is enabled.
317    pub async fn collect_paired_devices(&self) -> Result<Vec<DeviceConnection>, ReceiverError> {
318        // The idea here is that, when triggering fake device arrival notifications, the
319        // receiver will send the register write confirmation message only AFTER sending
320        // all arrival notifications.
321        // So we will trigger device arrival notifications and continue collecting those
322        // until the original future has completed.
323
324        let mut devices = vec![];
325
326        let rx = self.listen();
327        let fin = self.trigger_device_arrival().fuse();
328        pin_mut!(fin);
329
330        loop {
331            select! {
332                _ = fin => break,
333                res = rx.recv().fuse() => {
334                    let Ok(Event::DeviceConnection(connection)) = res else {
335                        continue;
336                    };
337
338                    devices.push(connection);
339                }
340            }
341        }
342
343        Ok(devices)
344    }
345
346    /// Retrieves the unique ID of the receiver. This is not the same as the
347    /// serial number.
348    pub async fn get_unique_id(&self) -> Result<String, ReceiverError> {
349        let response = self
350            .chan
351            .read_long_register(RECEIVER_DEVICE_INDEX, Register::UniqueId.into(), [0u8; 3])
352            .await?;
353
354        // When decoding the last 8 bytes of the response to their ASCII representation
355        // we seem to get a valid hex string representing 4 bytes of data.
356        // Interpreting this hex string as little endian we seem to get the same decimal
357        // value the Options+ software calls `udid` (unique device identifier?). I am
358        // not sure what this is about and it may be a (major) coincidence that these
359        // values match for my receiver, but it could be worth keeping this in mind.
360
361        // I have no clue how to retrieve the serial number of the receiver.
362
363        Ok(str::from_utf8(&response)
364            .map_err(|_| Hidpp10Error::UnsupportedResponse)?
365            .to_string())
366    }
367
368    /// Provides the pairing information of a specific paired device by its
369    /// index.
370    pub async fn get_device_pairing_information(
371        &self,
372        device_index: u8,
373    ) -> Result<DevicePairingInformation, ReceiverError> {
374        let response = self
375            .chan
376            .read_long_register(
377                RECEIVER_DEVICE_INDEX,
378                Register::ReceiverInfo.into(),
379                [
380                    u8::from(InfoSubRegister::DevicePairingInformation) + (device_index & 0x0f),
381                    0x00,
382                    0x00,
383                ],
384            )
385            .await?;
386
387        Ok(DevicePairingInformation {
388            wpid: u16::from_le_bytes(response[2..=3].try_into().unwrap()),
389            kind: DeviceKind::try_from(response[1] & 0x0f)
390                .map_err(|_| Hidpp10Error::UnsupportedResponse)?,
391            encrypted: response[1] & (1 << 5) != 0,
392            online: response[1] & (1 << 6) == 0,
393            unit_id: response[4..=7].try_into().unwrap(),
394        })
395    }
396
397    /// Provides the codename of a specific paired device by its index.
398    pub async fn get_device_codename(&self, device_index: u8) -> Result<String, ReceiverError> {
399        // For device names longer than 13 characters this may need to be called
400        // multiple times with different parameters. I don't have a device with
401        // such a name to be able to test this.
402
403        let response = self
404            .chan
405            .read_long_register(
406                RECEIVER_DEVICE_INDEX,
407                Register::ReceiverInfo.into(),
408                [
409                    u8::from(InfoSubRegister::DeviceCodename) + (device_index & 0x0f),
410                    0x01,
411                    0x00,
412                ],
413            )
414            .await?;
415
416        let end_idx = 3 + response[2] as usize;
417        Ok(str::from_utf8(&response[3..end_idx])
418            .map_err(|_| Hidpp10Error::UnsupportedResponse)?
419            .to_string())
420    }
421
422    /// Unpairs a device from the receiver by its index.
423    pub async fn unpair_device(&self, device_index: u8) -> Result<(), ReceiverError> {
424        let mut payload = [0u8; 16];
425        payload[0] = 0x03;
426        payload[1] = device_index;
427
428        self.chan
429            .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
430            .await?;
431
432        Ok(())
433    }
434
435    /// Starts the pairing process for a new device.
436    ///
437    /// The required `address` and `authentication` values are usually
438    /// discovered from the [`Event::DeviceDiscoveryDeviceDetails`] event which
439    /// is emitted regularly when actively discovering available devices
440    /// ([`Self::discover_devices`]).
441    ///
442    /// `entropy` specifies how complex the authentication passkey should be.
443    /// For mice, this defines the amount of keypresses (left or right) the user
444    /// has to perform. Not all values seem to be supported.
445    pub async fn pair_device(
446        &self,
447        slot: u8,
448        address: [u8; 6],
449        authentication: u8,
450        entropy: u8,
451    ) -> Result<(), ReceiverError> {
452        let mut payload = [0u8; 16];
453        payload[0] = 0x01;
454        payload[1] = slot;
455        payload[2..=7].copy_from_slice(&address);
456        payload[8] = authentication;
457        payload[9] = entropy;
458
459        self.chan
460            .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
461            .await?;
462
463        Ok(())
464    }
465
466    /// Starts device discovery for `timeout` seconds ([`None`] = default, seems
467    /// to be 30s). The maximum supported value is 60s.
468    ///
469    /// While device discovery is enabled,
470    /// [`Event::DeviceDiscoveryDeviceDetails`] and
471    /// [`Event::DeviceDiscoveryDeviceName`] events are emitted for every
472    /// discovered device.
473    pub async fn discover_devices(&self, timeout: Option<u8>) -> Result<(), ReceiverError> {
474        self.chan
475            .write_register(
476                RECEIVER_DEVICE_INDEX,
477                Register::DeviceDiscovery.into(),
478                [timeout.unwrap_or(0x00), 0x01, 0x00],
479            )
480            .await?;
481
482        Ok(())
483    }
484
485    /// Cancels the device discovery process.
486    pub async fn cancel_device_discovery(&self) -> Result<(), ReceiverError> {
487        self.chan
488            .write_register(
489                RECEIVER_DEVICE_INDEX,
490                Register::DeviceDiscovery.into(),
491                [0x00, 0x02, 0x00],
492            )
493            .await?;
494
495        Ok(())
496    }
497}
498
499impl Drop for Receiver {
500    fn drop(&mut self) {
501        self.chan.remove_msg_listener(self.msg_listener_hdl);
502    }
503}
504
505/// Indicates which notifications are enabled and thus sent by the receiver.
506///
507/// This information can be queried using [`Receiver::get_notification_state`].
508#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Builder)]
509#[cfg_attr(feature = "serde", derive(serde::Serialize))]
510#[non_exhaustive]
511pub struct NotificationState {
512    /// Whether the receiver sends device arrival/removal notifications.
513    pub wireless_notifications: bool,
514}
515
516/// Represents information about a paired device.
517///
518/// This information can be queried using
519/// [`Receiver::get_device_pairing_information`].
520#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
521#[cfg_attr(feature = "serde", derive(serde::Serialize))]
522#[non_exhaustive]
523pub struct DevicePairingInformation {
524    pub wpid: u16,
525    pub kind: DeviceKind,
526    pub encrypted: bool,
527    pub online: bool,
528    pub unit_id: [u8; 4],
529}
530
531/// Represents the kind of a device paired to a Bolt receiver.
532#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
533#[cfg_attr(feature = "serde", derive(serde::Serialize))]
534#[non_exhaustive]
535#[repr(u8)]
536pub enum DeviceKind {
537    Unknown = 0x00,
538    Keyboard = 0x01,
539    Mouse = 0x02,
540    Numpad = 0x03,
541    Presenter = 0x04,
542    Remote = 0x07,
543    Trackball = 0x08,
544    Touchpad = 0x09,
545    Tablet = 0x0a,
546    Gamepad = 0x0b,
547    Joystick = 0x0c,
548    Headset = 0x0d,
549}
550
551/// Represents an event emitted by the receiver.
552///
553/// You can listen to these events using [`Receiver::listen`]. Only enabled
554/// notifications as indicated by [`Receiver::get_notification_state`] are
555/// emitted.
556#[derive(Clone, PartialEq, Eq, Hash, Debug)]
557#[cfg_attr(feature = "serde", derive(serde::Serialize))]
558#[non_exhaustive]
559pub enum Event {
560    /// Is emitted whenever a device connects to or disconnects from the
561    /// receiver, but only if [`NotificationState::wireless_notifications`] is
562    /// enabled.
563    ///
564    /// Can be triggered for all paired devices using
565    /// [`Receiver::trigger_device_arrival`] to allow easy device enumeration.
566    ///
567    /// [`Receiver::collect_paired_devices`] implements a simple mechanism to
568    /// collect all paired devices.
569    DeviceConnection(DeviceConnection),
570
571    /// Is emitted whenever the device discovery status changes.
572    DeviceDiscoveryStatus { discovery_enabled: bool },
573
574    /// Is emitted many times for every device discovered using
575    /// [`Receiver::discover_devices`].
576    ///
577    /// This event contains device details, including its address required to
578    /// start pairing. The [`Event::DeviceDiscoveryDeviceName`] event will also
579    /// be emitted and contains the device name.
580    DeviceDiscoveryDeviceDetails {
581        /// The incrementing event counter. This can be used to map
582        /// [`Event::DeviceDiscoveryDeviceDetails`] and
583        /// [`Event::DeviceDiscoveryDeviceName`] events.
584        counter: u16,
585
586        kind: DeviceKind,
587        wpid: u16,
588
589        /// The address of the device required to pair it using
590        /// [`Receiver::pair_device`].
591        ///
592        /// This can also be used as the unique device identifier when
593        /// collecting discovered devices.
594        address: [u8; 6],
595
596        /// The authentication type(s) the device supports. Unfortunately, there
597        /// is not much information about this value and whether it is a
598        /// single value or a bitfield.
599        authentication: u8,
600    },
601
602    /// Is emitted many times for every device discovered using
603    /// [`Receiver::discover_devices`].
604    ///
605    /// This event only contains the device name. Device details will be
606    /// provided using the [`Event::DeviceDiscoveryDeviceDetails`] event.
607    DeviceDiscoveryDeviceName {
608        /// The incrementing event counter. This can be used to map
609        /// [`Event::DeviceDiscoveryDeviceDetails`] and
610        /// [`Event::DeviceDiscoveryDeviceName`] events.
611        counter: u16,
612
613        name: String,
614    },
615
616    /// Is emitted whenever the status of a pairing process changes.
617    PairingStatus {
618        device_address: [u8; 6],
619        pairing_error: Option<PairingError>,
620
621        /// The receiver slot the newly paired device was paired to. This can be
622        /// used as the device index for subsequent operations.
623        slot: Option<u8>,
624    },
625
626    /// Is emitted once the receiver requests a passkey to be entered on a
627    /// device that should be paired to it.
628    PairingPasskeyRequest {
629        device_address: [u8; 6],
630
631        /// The passkey the user has to enter in order to pair the device.
632        ///
633        /// Depending on the device and authentication type, this value has
634        /// different implications.
635        ///
636        /// For mice, this value will be a valid 6-digit number. After parsing
637        /// this into an integer, the (least significant) bits represent
638        /// the sequence of mouse presses (`0` = left, `1` = right) the
639        /// user has to perform, with an additional press of both mouse
640        /// buttons simultaneously.
641        ///
642        /// The amount of bits significant to this equals to the `entropy`
643        /// passed to [`Receiver::pair_device`].
644        passkey: String,
645    },
646
647    /// Is emitted for every keypress a user performs while entering a pairing
648    /// passkey.
649    PairingPasskeyPressed {
650        device_address: [u8; 6],
651
652        /// The type of the keypress the user performed.
653        ///
654        /// Every passkey sequence starts with an event where this value is set
655        /// to [`PairingPasskeyPressType::Initialization`]. Each time the user
656        /// presses a key, an event with a press type of
657        /// [`PairingPasskeyPressType::Keypress`] is emitted. Once the user
658        /// submits their passkey, this value will be
659        /// [`PairingPasskeyPressType::Submit`].
660        press_type: PairingPasskeyPressType,
661    },
662}
663
664/// Represents a device connected to a Bolt receiver.
665///
666/// This information is emitted by the [`Event::DeviceConnection`] event and can
667/// be conveniently collected using [`Receiver::collect_paired_devices`].
668#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
669#[cfg_attr(feature = "serde", derive(serde::Serialize))]
670#[non_exhaustive]
671pub struct DeviceConnection {
672    pub index: u8,
673    pub kind: DeviceKind,
674    pub encrypted: bool,
675    pub online: bool,
676    pub wpid: u16,
677}
678
679/// Represents an error during device pairing.
680///
681/// This is reported by the [`Event::PairingStatus`] event.
682#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, TryFromPrimitive, IntoPrimitive)]
683#[cfg_attr(feature = "serde", derive(serde::Serialize))]
684#[non_exhaustive]
685#[repr(u8)]
686pub enum PairingError {
687    DeviceTimeout = 0x01,
688    Failed = 0x02,
689}
690
691/// Represents the type of a single passkey press.
692///
693/// This is reported by the [`Event::PairingPasskeyPressed`] event, which also
694/// includes some further information about the context of these values.
695#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, TryFromPrimitive, IntoPrimitive)]
696#[cfg_attr(feature = "serde", derive(serde::Serialize))]
697#[non_exhaustive]
698#[repr(u8)]
699pub enum PairingPasskeyPressType {
700    Initialization = 0x00,
701    Keypress = 0x01,
702    Submit = 0x04,
703}