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};
18use openlogi_device_registry::receiver::{ReceiverProtocol, find_receiver};
19
20use super::{RECEIVER_DEVICE_INDEX, ReceiverError};
21use crate::{
22    channel::{HidppChannel, MessageListenerGuard},
23    emitter::EventEmitter,
24    protocol::v10::{self, Hidpp10Error},
25};
26
27mod event;
28
29pub use event::{
30    DeviceConnection, DeviceKind, Event, PairingPasskeyPressType, decode as decode_notification,
31};
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 find_receiver(chan.vendor_id, chan.product_id)
110            .is_none_or(|receiver| receiver.protocol != ReceiverProtocol::Bolt)
111        {
112            return Err(ReceiverError::UnknownReceiver);
113        }
114
115        let emitter = Arc::new(EventEmitter::new());
116
117        let listener = chan.add_msg_listener_guarded({
118            let emitter = Arc::clone(&emitter);
119
120            move |raw, matched| {
121                // A report already matched to an outgoing request is a
122                // response, not a notification.
123                if matched {
124                    return;
125                }
126
127                if let Some(event) = event::decode(&v10::Message::from(raw)) {
128                    emitter.emit(event);
129                }
130            }
131        });
132
133        Ok(Receiver {
134            _listener: Arc::new(listener),
135            chan,
136            emitter,
137        })
138    }
139
140    /// Creates a new listener for receiving receiver events.
141    #[must_use]
142    pub fn listen(&self) -> async_channel::Receiver<Event> {
143        self.emitter.create_receiver()
144    }
145
146    /// Queries the current information about what notifications are enabled.
147    pub async fn get_notification_state(&self) -> Result<NotificationState, ReceiverError> {
148        let response = self
149            .chan
150            .read_register(
151                RECEIVER_DEVICE_INDEX,
152                Register::Notifications.into(),
153                [0u8; 3],
154            )
155            .await?;
156
157        Ok(NotificationState {
158            wireless_notifications: (response[1] & 1) != 0,
159        })
160    }
161
162    /// Configures what notifications are enabled and thus reported by the
163    /// receiver.
164    pub async fn set_notification_state(
165        &self,
166        state: NotificationState,
167    ) -> Result<(), ReceiverError> {
168        self.chan
169            .write_register(
170                RECEIVER_DEVICE_INDEX,
171                Register::Notifications.into(),
172                [0, u8::from(state.wireless_notifications), 0],
173            )
174            .await?;
175
176        Ok(())
177    }
178
179    /// Counts the amount of devices currently paired to this receiver. The
180    /// devices don't have to be online to be included here as pairings are
181    /// persistent.
182    pub async fn count_pairings(&self) -> Result<u8, ReceiverError> {
183        let response = self
184            .chan
185            .read_register(
186                RECEIVER_DEVICE_INDEX,
187                Register::Connections.into(),
188                [0u8; 3],
189            )
190            .await?;
191
192        Ok(response[1])
193    }
194
195    /// Triggers device arrival notifications for all devices currently
196    /// connected to the receiver. This is useful for device enumeration.
197    ///
198    /// Check [`Self::get_notification_state`] first to make sure that
199    /// [`NotificationState::wireless_notifications`] is enabled.
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    /// Collects information about all paired devices by calling
213    /// [`Self::trigger_device_arrival`] and collecting incoming
214    /// [`Event::DeviceConnection`] events.
215    ///
216    /// Check [`Self::get_notification_state`] first to make sure that
217    /// [`NotificationState::wireless_notifications`] is enabled.
218    pub async fn collect_paired_devices(&self) -> Result<Vec<DeviceConnection>, ReceiverError> {
219        // The idea here is that, when triggering fake device arrival notifications, the
220        // receiver will send the register write confirmation message only AFTER sending
221        // all arrival notifications.
222        // So we will trigger device arrival notifications and continue collecting those
223        // until the original future has completed.
224
225        let mut devices = vec![];
226
227        let rx = self.listen();
228        let fin = self.trigger_device_arrival().fuse();
229        pin_mut!(fin);
230
231        loop {
232            select! {
233                _ = fin => break,
234                res = rx.recv().fuse() => {
235                    let Ok(Event::DeviceConnection(connection)) = res else {
236                        continue;
237                    };
238
239                    devices.push(connection);
240                }
241            }
242        }
243
244        Ok(devices)
245    }
246
247    /// Retrieves the unique ID of the receiver. This is not the same as the
248    /// serial number.
249    pub async fn get_unique_id(&self) -> Result<String, ReceiverError> {
250        let response = self
251            .chan
252            .read_long_register(RECEIVER_DEVICE_INDEX, Register::UniqueId.into(), [0u8; 3])
253            .await?;
254
255        // When decoding the last 8 bytes of the response to their ASCII representation
256        // we seem to get a valid hex string representing 4 bytes of data.
257        // Interpreting this hex string as little endian we seem to get the same decimal
258        // value the Options+ software calls `udid` (unique device identifier?). I am
259        // not sure what this is about and it may be a (major) coincidence that these
260        // values match for my receiver, but it could be worth keeping this in mind.
261
262        // I have no clue how to retrieve the serial number of the receiver.
263
264        Ok(str::from_utf8(&response)
265            .map_err(|_| Hidpp10Error::UnsupportedResponse)?
266            .to_string())
267    }
268
269    /// Provides the pairing information of a specific paired device by its
270    /// index.
271    pub async fn get_device_pairing_information(
272        &self,
273        device_index: u8,
274    ) -> Result<DevicePairingInformation, ReceiverError> {
275        let response = self
276            .chan
277            .read_long_sub_register(
278                RECEIVER_DEVICE_INDEX,
279                Register::ReceiverInfo.into(),
280                u8::from(InfoSubRegister::DevicePairingInformation) + (device_index & 0x0f),
281                [0x00, 0x00],
282            )
283            .await?;
284
285        Ok(DevicePairingInformation {
286            wpid: u16::from_le_bytes([response[2], response[3]]),
287            // Kind is identity-only: an unrecognised nibble folds to
288            // `Unknown` instead of failing the whole pairing-info read.
289            kind: DeviceKind::from(response[1] & 0x0f),
290            encrypted: response[1] & (1 << 5) != 0,
291            online: response[1] & (1 << 6) == 0,
292            unit_id: [response[4], response[5], response[6], response[7]],
293        })
294    }
295
296    /// Provides the codename of a specific paired device by its index.
297    pub async fn get_device_codename(&self, device_index: u8) -> Result<String, ReceiverError> {
298        // For device names longer than 13 characters this may need to be called
299        // multiple times with different parameters. I don't have a device with
300        // such a name to be able to test this.
301
302        let response = self
303            .chan
304            .read_long_sub_register(
305                RECEIVER_DEVICE_INDEX,
306                Register::ReceiverInfo.into(),
307                u8::from(InfoSubRegister::DeviceCodename) + (device_index & 0x0f),
308                [0x01, 0x00],
309            )
310            .await?;
311
312        Ok(parse_codename(&response)
313            .ok_or(Hidpp10Error::UnsupportedResponse)?
314            .to_string())
315    }
316
317    /// Unpairs a device from the receiver by its index.
318    pub async fn unpair_device(&self, device_index: u8) -> Result<(), ReceiverError> {
319        let mut payload = [0u8; 16];
320        payload[0] = 0x03;
321        payload[1] = device_index;
322
323        self.chan
324            .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
325            .await?;
326
327        Ok(())
328    }
329
330    /// Starts the pairing process for a new device.
331    ///
332    /// The required `address` and `authentication` values are usually
333    /// discovered from the [`Event::DeviceDiscoveryDeviceDetails`] event which
334    /// is emitted regularly when actively discovering available devices
335    /// ([`Self::discover_devices`]).
336    ///
337    /// `entropy` specifies how complex the authentication passkey should be.
338    /// For mice, this defines the amount of keypresses (left or right) the user
339    /// has to perform. Not all values seem to be supported.
340    pub async fn pair_device(
341        &self,
342        slot: u8,
343        address: [u8; 6],
344        authentication: u8,
345        entropy: u8,
346    ) -> Result<(), ReceiverError> {
347        let mut payload = [0u8; 16];
348        payload[0] = 0x01;
349        payload[1] = slot;
350        payload[2..=7].copy_from_slice(&address);
351        payload[8] = authentication;
352        payload[9] = entropy;
353
354        self.chan
355            .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
356            .await?;
357
358        Ok(())
359    }
360
361    /// Starts device discovery for `timeout` seconds ([`None`] = default, seems
362    /// to be 30s). The maximum supported value is 60s.
363    ///
364    /// While device discovery is enabled,
365    /// [`Event::DeviceDiscoveryDeviceDetails`] and
366    /// [`Event::DeviceDiscoveryDeviceName`] events are emitted for every
367    /// discovered device.
368    pub async fn discover_devices(&self, timeout: Option<u8>) -> Result<(), ReceiverError> {
369        self.chan
370            .write_register(
371                RECEIVER_DEVICE_INDEX,
372                Register::DeviceDiscovery.into(),
373                [timeout.unwrap_or(0x00), 0x01, 0x00],
374            )
375            .await?;
376
377        Ok(())
378    }
379
380    /// Cancels the device discovery process.
381    pub async fn cancel_device_discovery(&self) -> Result<(), ReceiverError> {
382        self.chan
383            .write_register(
384                RECEIVER_DEVICE_INDEX,
385                Register::DeviceDiscovery.into(),
386                [0x00, 0x02, 0x00],
387            )
388            .await?;
389
390        Ok(())
391    }
392}
393
394/// Extract the codename chunk from a `DeviceCodename` register read.
395///
396/// `response[2]` is the device-reported name length. A name longer than the
397/// 13 bytes one response carries is clamped to the chunk present (fetching
398/// the rest takes further reads with different parameters); a length byte
399/// pointing past the response must not panic. `None` for non-UTF-8 bytes.
400fn parse_codename(response: &[u8; 16]) -> Option<&str> {
401    let end = 3usize.saturating_add(usize::from(response[2]));
402    let raw = response.get(3..end.min(response.len()))?;
403    str::from_utf8(raw).ok()
404}
405
406/// Indicates which notifications are enabled and thus sent by the receiver.
407///
408/// This information can be queried using [`Receiver::get_notification_state`].
409#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Builder)]
410#[cfg_attr(feature = "serde", derive(serde::Serialize))]
411#[non_exhaustive]
412pub struct NotificationState {
413    /// Whether the receiver sends device arrival/removal notifications.
414    pub wireless_notifications: bool,
415}
416
417/// Represents information about a paired device.
418///
419/// This information can be queried using
420/// [`Receiver::get_device_pairing_information`].
421#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
422#[cfg_attr(feature = "serde", derive(serde::Serialize))]
423#[non_exhaustive]
424pub struct DevicePairingInformation {
425    /// Wireless product ID of the paired device.
426    pub wpid: u16,
427    /// Device kind reported by the receiver.
428    pub kind: DeviceKind,
429    /// Whether the link is encrypted.
430    pub encrypted: bool,
431    /// Whether the device is currently online.
432    pub online: bool,
433    /// Device unit ID.
434    pub unit_id: [u8; 4],
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440
441    #[test]
442    fn codename_with_oversized_length_clamps_to_available_chunk() {
443        let mut response = [0u8; 16];
444        response[2] = 200;
445        response[3..16].copy_from_slice(b"MX Anywhere 3");
446
447        assert_eq!(parse_codename(&response), Some("MX Anywhere 3"));
448    }
449
450    #[test]
451    fn codename_within_bounds_parses() {
452        let mut response = [0u8; 16];
453        response[2] = 5;
454        response[3..8].copy_from_slice(b"Casa!");
455
456        assert_eq!(parse_codename(&response), Some("Casa!"));
457    }
458
459    #[test]
460    fn codename_rejects_invalid_utf8() {
461        let mut response = [0u8; 16];
462        response[2] = 2;
463        response[3] = 0xff;
464        response[4] = 0xfe;
465
466        assert_eq!(parse_codename(&response), None);
467    }
468}