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_register(
278                RECEIVER_DEVICE_INDEX,
279                Register::ReceiverInfo.into(),
280                [
281                    u8::from(InfoSubRegister::DevicePairingInformation) + (device_index & 0x0f),
282                    0x00,
283                    0x00,
284                ],
285            )
286            .await?;
287
288        Ok(DevicePairingInformation {
289            wpid: u16::from_le_bytes([response[2], response[3]]),
290            // Kind is identity-only: an unrecognised nibble folds to
291            // `Unknown` instead of failing the whole pairing-info read.
292            kind: DeviceKind::from(response[1] & 0x0f),
293            encrypted: response[1] & (1 << 5) != 0,
294            online: response[1] & (1 << 6) == 0,
295            unit_id: [response[4], response[5], response[6], response[7]],
296        })
297    }
298
299    /// Provides the codename of a specific paired device by its index.
300    pub async fn get_device_codename(&self, device_index: u8) -> Result<String, ReceiverError> {
301        // For device names longer than 13 characters this may need to be called
302        // multiple times with different parameters. I don't have a device with
303        // such a name to be able to test this.
304
305        let response = self
306            .chan
307            .read_long_register(
308                RECEIVER_DEVICE_INDEX,
309                Register::ReceiverInfo.into(),
310                [
311                    u8::from(InfoSubRegister::DeviceCodename) + (device_index & 0x0f),
312                    0x01,
313                    0x00,
314                ],
315            )
316            .await?;
317
318        Ok(parse_codename(&response)
319            .ok_or(Hidpp10Error::UnsupportedResponse)?
320            .to_string())
321    }
322
323    /// Unpairs a device from the receiver by its index.
324    pub async fn unpair_device(&self, device_index: u8) -> Result<(), ReceiverError> {
325        let mut payload = [0u8; 16];
326        payload[0] = 0x03;
327        payload[1] = device_index;
328
329        self.chan
330            .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
331            .await?;
332
333        Ok(())
334    }
335
336    /// Starts the pairing process for a new device.
337    ///
338    /// The required `address` and `authentication` values are usually
339    /// discovered from the [`Event::DeviceDiscoveryDeviceDetails`] event which
340    /// is emitted regularly when actively discovering available devices
341    /// ([`Self::discover_devices`]).
342    ///
343    /// `entropy` specifies how complex the authentication passkey should be.
344    /// For mice, this defines the amount of keypresses (left or right) the user
345    /// has to perform. Not all values seem to be supported.
346    pub async fn pair_device(
347        &self,
348        slot: u8,
349        address: [u8; 6],
350        authentication: u8,
351        entropy: u8,
352    ) -> Result<(), ReceiverError> {
353        let mut payload = [0u8; 16];
354        payload[0] = 0x01;
355        payload[1] = slot;
356        payload[2..=7].copy_from_slice(&address);
357        payload[8] = authentication;
358        payload[9] = entropy;
359
360        self.chan
361            .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
362            .await?;
363
364        Ok(())
365    }
366
367    /// Starts device discovery for `timeout` seconds ([`None`] = default, seems
368    /// to be 30s). The maximum supported value is 60s.
369    ///
370    /// While device discovery is enabled,
371    /// [`Event::DeviceDiscoveryDeviceDetails`] and
372    /// [`Event::DeviceDiscoveryDeviceName`] events are emitted for every
373    /// discovered device.
374    pub async fn discover_devices(&self, timeout: Option<u8>) -> Result<(), ReceiverError> {
375        self.chan
376            .write_register(
377                RECEIVER_DEVICE_INDEX,
378                Register::DeviceDiscovery.into(),
379                [timeout.unwrap_or(0x00), 0x01, 0x00],
380            )
381            .await?;
382
383        Ok(())
384    }
385
386    /// Cancels the device discovery process.
387    pub async fn cancel_device_discovery(&self) -> Result<(), ReceiverError> {
388        self.chan
389            .write_register(
390                RECEIVER_DEVICE_INDEX,
391                Register::DeviceDiscovery.into(),
392                [0x00, 0x02, 0x00],
393            )
394            .await?;
395
396        Ok(())
397    }
398}
399
400/// Extract the codename chunk from a `DeviceCodename` register read.
401///
402/// `response[2]` is the device-reported name length. A name longer than the
403/// 13 bytes one response carries is clamped to the chunk present (fetching
404/// the rest takes further reads with different parameters); a length byte
405/// pointing past the response must not panic. `None` for non-UTF-8 bytes.
406fn parse_codename(response: &[u8; 16]) -> Option<&str> {
407    let end = 3usize.saturating_add(usize::from(response[2]));
408    let raw = response.get(3..end.min(response.len()))?;
409    str::from_utf8(raw).ok()
410}
411
412/// Indicates which notifications are enabled and thus sent by the receiver.
413///
414/// This information can be queried using [`Receiver::get_notification_state`].
415#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Builder)]
416#[cfg_attr(feature = "serde", derive(serde::Serialize))]
417#[non_exhaustive]
418pub struct NotificationState {
419    /// Whether the receiver sends device arrival/removal notifications.
420    pub wireless_notifications: bool,
421}
422
423/// Represents information about a paired device.
424///
425/// This information can be queried using
426/// [`Receiver::get_device_pairing_information`].
427#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
428#[cfg_attr(feature = "serde", derive(serde::Serialize))]
429#[non_exhaustive]
430pub struct DevicePairingInformation {
431    /// Wireless product ID of the paired device.
432    pub wpid: u16,
433    /// Device kind reported by the receiver.
434    pub kind: DeviceKind,
435    /// Whether the link is encrypted.
436    pub encrypted: bool,
437    /// Whether the device is currently online.
438    pub online: bool,
439    /// Device unit ID.
440    pub unit_id: [u8; 4],
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    #[test]
448    fn codename_with_oversized_length_clamps_to_available_chunk() {
449        let mut response = [0u8; 16];
450        response[2] = 200;
451        response[3..16].copy_from_slice(b"MX Anywhere 3");
452
453        assert_eq!(parse_codename(&response), Some("MX Anywhere 3"));
454    }
455
456    #[test]
457    fn codename_within_bounds_parses() {
458        let mut response = [0u8; 16];
459        response[2] = 5;
460        response[3..8].copy_from_slice(b"Casa!");
461
462        assert_eq!(parse_codename(&response), Some("Casa!"));
463    }
464
465    #[test]
466    fn codename_rejects_invalid_utf8() {
467        let mut response = [0u8; 16];
468        response[2] = 2;
469        response[3] = 0xff;
470        response[4] = 0xfe;
471
472        assert_eq!(parse_codename(&response), None);
473    }
474}