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::{FromPrimitive, 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
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    _listener: Arc<MessageListenerGuard>,
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 listener = chan.add_msg_listener_guarded({
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                        // Kind is identity-only; an unrecognised nibble folds
131                        // to `Unknown` instead of dropping the event.
132                        emitter.emit(Event::DeviceConnection(DeviceConnection {
133                            index: header.device_index,
134                            kind: DeviceKind::from(payload[1] & 0x0f),
135                            encrypted: payload[1] & (1 << 5) != 0,
136                            online: payload[1] & (1 << 6) == 0,
137                            wpid: u16::from_le_bytes(payload[2..=3].try_into().unwrap()),
138                        }));
139                    }
140                    // Device discovery
141                    0x4f => {
142                        match payload[2] {
143                            // Device data
144                            0 => {
145                                emitter.emit(Event::DeviceDiscoveryDeviceDetails {
146                                    counter: payload[0] as u16 + payload[1] as u16 * 256,
147                                    kind: DeviceKind::from(payload[4] & 0x0f),
148                                    wpid: u16::from_le_bytes(payload[5..=6].try_into().unwrap()),
149                                    address: payload[7..=12].try_into().unwrap(),
150                                    authentication: payload[15],
151                                });
152                            }
153                            // Device name
154                            1 => {
155                                let Some((counter, name)) = parse_discovery_name(&payload) else {
156                                    return;
157                                };
158
159                                emitter.emit(Event::DeviceDiscoveryDeviceName {
160                                    counter,
161                                    name: name.to_string(),
162                                });
163                            }
164                            _ => (),
165                        }
166                    }
167                    // Device discovery status
168                    0x53 => {
169                        emitter.emit(Event::DeviceDiscoveryStatus {
170                            discovery_enabled: payload[0] == 0x00,
171                        });
172                    }
173                    // Pairing status
174                    0x54 => {
175                        // payload[0] contains some kind of information about the status. I don't
176                        // know how to map that though.
177
178                        // An unrecognised error code still means "pairing
179                        // failed" — dropping it here would turn the failure
180                        // into a session timeout. Carry the raw code instead.
181                        let error = (payload[1] != 0x00).then(|| PairingError::from(payload[1]));
182
183                        emitter.emit(Event::PairingStatus {
184                            device_address: payload[2..=7].try_into().unwrap(),
185                            pairing_error: error,
186                            slot: if payload[8] == 0x00 {
187                                None
188                            } else {
189                                Some(payload[8])
190                            },
191                        });
192                    }
193                    // Passkey request
194                    0x4d => {
195                        // 6 bytes, NUL-padded when the passkey is shorter.
196                        let digits = &payload[1..=6];
197                        let len = digits.iter().position(|&b| b == 0).unwrap_or(digits.len());
198                        let Ok(passkey) = str::from_utf8(&digits[..len]) else {
199                            return;
200                        };
201
202                        emitter.emit(Event::PairingPasskeyRequest {
203                            device_address: payload[7..=12].try_into().unwrap(),
204                            passkey: passkey.to_string(),
205                        });
206                    }
207                    // Passkey pressed
208                    0x4e => {
209                        emitter.emit(Event::PairingPasskeyPressed {
210                            device_address: payload[1..=6].try_into().unwrap(),
211                            press_type: PairingPasskeyPressType::from(payload[0]),
212                        });
213                    }
214                    _ => (),
215                }
216            }
217        });
218
219        Ok(Receiver {
220            _listener: Arc::new(listener),
221            chan,
222            emitter,
223        })
224    }
225
226    /// Creates a new listener for receiving receiver events.
227    pub fn listen(&self) -> async_channel::Receiver<Event> {
228        self.emitter.create_receiver()
229    }
230
231    /// Queries the current information about what notifications are enabled.
232    pub async fn get_notification_state(&self) -> Result<NotificationState, ReceiverError> {
233        let response = self
234            .chan
235            .read_register(
236                RECEIVER_DEVICE_INDEX,
237                Register::Notifications.into(),
238                [0u8; 3],
239            )
240            .await?;
241
242        Ok(NotificationState {
243            wireless_notifications: (response[1] & 1) != 0,
244        })
245    }
246
247    /// Configures what notifications are enabled and thus reported by the
248    /// receiver.
249    pub async fn set_notification_state(
250        &self,
251        state: NotificationState,
252    ) -> Result<(), ReceiverError> {
253        self.chan
254            .write_register(
255                RECEIVER_DEVICE_INDEX,
256                Register::Notifications.into(),
257                [0, if state.wireless_notifications { 1 } else { 0 }, 0],
258            )
259            .await?;
260
261        Ok(())
262    }
263
264    /// Counts the amount of devices currently paired to this receiver. The
265    /// devices don't have to be online to be included here as pairings are
266    /// persistent.
267    pub async fn count_pairings(&self) -> Result<u8, ReceiverError> {
268        let response = self
269            .chan
270            .read_register(
271                RECEIVER_DEVICE_INDEX,
272                Register::Connections.into(),
273                [0u8; 3],
274            )
275            .await?;
276
277        Ok(response[1])
278    }
279
280    /// Triggers device arrival notifications for all devices currently
281    /// connected to the receiver. This is useful for device enumeration.
282    ///
283    /// Check [`Self::get_notification_state`] first to make sure that
284    /// [`NotificationState::wireless_notifications`] is enabled.
285    pub async fn trigger_device_arrival(&self) -> Result<(), ReceiverError> {
286        self.chan
287            .write_register(
288                RECEIVER_DEVICE_INDEX,
289                Register::Connections.into(),
290                [0x02, 0x00, 0x00],
291            )
292            .await?;
293
294        Ok(())
295    }
296
297    /// Collects information about all paired devices by calling
298    /// [`Self::trigger_device_arrival`] and collecting incoming
299    /// [`Event::DeviceConnection`] events.
300    ///
301    /// Check [`Self::get_notification_state`] first to make sure that
302    /// [`NotificationState::wireless_notifications`] is enabled.
303    pub async fn collect_paired_devices(&self) -> Result<Vec<DeviceConnection>, ReceiverError> {
304        // The idea here is that, when triggering fake device arrival notifications, the
305        // receiver will send the register write confirmation message only AFTER sending
306        // all arrival notifications.
307        // So we will trigger device arrival notifications and continue collecting those
308        // until the original future has completed.
309
310        let mut devices = vec![];
311
312        let rx = self.listen();
313        let fin = self.trigger_device_arrival().fuse();
314        pin_mut!(fin);
315
316        loop {
317            select! {
318                _ = fin => break,
319                res = rx.recv().fuse() => {
320                    let Ok(Event::DeviceConnection(connection)) = res else {
321                        continue;
322                    };
323
324                    devices.push(connection);
325                }
326            }
327        }
328
329        Ok(devices)
330    }
331
332    /// Retrieves the unique ID of the receiver. This is not the same as the
333    /// serial number.
334    pub async fn get_unique_id(&self) -> Result<String, ReceiverError> {
335        let response = self
336            .chan
337            .read_long_register(RECEIVER_DEVICE_INDEX, Register::UniqueId.into(), [0u8; 3])
338            .await?;
339
340        // When decoding the last 8 bytes of the response to their ASCII representation
341        // we seem to get a valid hex string representing 4 bytes of data.
342        // Interpreting this hex string as little endian we seem to get the same decimal
343        // value the Options+ software calls `udid` (unique device identifier?). I am
344        // not sure what this is about and it may be a (major) coincidence that these
345        // values match for my receiver, but it could be worth keeping this in mind.
346
347        // I have no clue how to retrieve the serial number of the receiver.
348
349        Ok(str::from_utf8(&response)
350            .map_err(|_| Hidpp10Error::UnsupportedResponse)?
351            .to_string())
352    }
353
354    /// Provides the pairing information of a specific paired device by its
355    /// index.
356    pub async fn get_device_pairing_information(
357        &self,
358        device_index: u8,
359    ) -> Result<DevicePairingInformation, ReceiverError> {
360        let response = self
361            .chan
362            .read_long_register(
363                RECEIVER_DEVICE_INDEX,
364                Register::ReceiverInfo.into(),
365                [
366                    u8::from(InfoSubRegister::DevicePairingInformation) + (device_index & 0x0f),
367                    0x00,
368                    0x00,
369                ],
370            )
371            .await?;
372
373        Ok(DevicePairingInformation {
374            wpid: u16::from_le_bytes(response[2..=3].try_into().unwrap()),
375            // Kind is identity-only: an unrecognised nibble folds to
376            // `Unknown` instead of failing the whole pairing-info read.
377            kind: DeviceKind::from(response[1] & 0x0f),
378            encrypted: response[1] & (1 << 5) != 0,
379            online: response[1] & (1 << 6) == 0,
380            unit_id: response[4..=7].try_into().unwrap(),
381        })
382    }
383
384    /// Provides the codename of a specific paired device by its index.
385    pub async fn get_device_codename(&self, device_index: u8) -> Result<String, ReceiverError> {
386        // For device names longer than 13 characters this may need to be called
387        // multiple times with different parameters. I don't have a device with
388        // such a name to be able to test this.
389
390        let response = self
391            .chan
392            .read_long_register(
393                RECEIVER_DEVICE_INDEX,
394                Register::ReceiverInfo.into(),
395                [
396                    u8::from(InfoSubRegister::DeviceCodename) + (device_index & 0x0f),
397                    0x01,
398                    0x00,
399                ],
400            )
401            .await?;
402
403        Ok(parse_codename(&response)
404            .ok_or(Hidpp10Error::UnsupportedResponse)?
405            .to_string())
406    }
407
408    /// Unpairs a device from the receiver by its index.
409    pub async fn unpair_device(&self, device_index: u8) -> Result<(), ReceiverError> {
410        let mut payload = [0u8; 16];
411        payload[0] = 0x03;
412        payload[1] = device_index;
413
414        self.chan
415            .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
416            .await?;
417
418        Ok(())
419    }
420
421    /// Starts the pairing process for a new device.
422    ///
423    /// The required `address` and `authentication` values are usually
424    /// discovered from the [`Event::DeviceDiscoveryDeviceDetails`] event which
425    /// is emitted regularly when actively discovering available devices
426    /// ([`Self::discover_devices`]).
427    ///
428    /// `entropy` specifies how complex the authentication passkey should be.
429    /// For mice, this defines the amount of keypresses (left or right) the user
430    /// has to perform. Not all values seem to be supported.
431    pub async fn pair_device(
432        &self,
433        slot: u8,
434        address: [u8; 6],
435        authentication: u8,
436        entropy: u8,
437    ) -> Result<(), ReceiverError> {
438        let mut payload = [0u8; 16];
439        payload[0] = 0x01;
440        payload[1] = slot;
441        payload[2..=7].copy_from_slice(&address);
442        payload[8] = authentication;
443        payload[9] = entropy;
444
445        self.chan
446            .write_long_register(RECEIVER_DEVICE_INDEX, Register::Pairing.into(), payload)
447            .await?;
448
449        Ok(())
450    }
451
452    /// Starts device discovery for `timeout` seconds ([`None`] = default, seems
453    /// to be 30s). The maximum supported value is 60s.
454    ///
455    /// While device discovery is enabled,
456    /// [`Event::DeviceDiscoveryDeviceDetails`] and
457    /// [`Event::DeviceDiscoveryDeviceName`] events are emitted for every
458    /// discovered device.
459    pub async fn discover_devices(&self, timeout: Option<u8>) -> Result<(), ReceiverError> {
460        self.chan
461            .write_register(
462                RECEIVER_DEVICE_INDEX,
463                Register::DeviceDiscovery.into(),
464                [timeout.unwrap_or(0x00), 0x01, 0x00],
465            )
466            .await?;
467
468        Ok(())
469    }
470
471    /// Cancels the device discovery process.
472    pub async fn cancel_device_discovery(&self) -> Result<(), ReceiverError> {
473        self.chan
474            .write_register(
475                RECEIVER_DEVICE_INDEX,
476                Register::DeviceDiscovery.into(),
477                [0x00, 0x02, 0x00],
478            )
479            .await?;
480
481        Ok(())
482    }
483}
484
485/// Parse a device-discovery name notification (sub-id `0x4f`, kind `1`).
486///
487/// `payload[3]` is the device-reported name length. The byte comes straight
488/// off the radio, so it must never index past the report: a length that does
489/// not fit the packet (or non-UTF-8 bytes) drops the event instead of
490/// panicking the listener.
491fn parse_discovery_name(payload: &[u8; 17]) -> Option<(u16, &str)> {
492    let len = usize::from(payload[3]);
493    let end = 4usize.checked_add(len)?;
494    let name = str::from_utf8(payload.get(4..end)?).ok()?;
495    Some((payload[0] as u16 + payload[1] as u16 * 256, name))
496}
497
498/// Extract the codename chunk from a `DeviceCodename` register read.
499///
500/// `response[2]` is the device-reported name length. A name longer than the
501/// 13 bytes one response carries is clamped to the chunk present (fetching
502/// the rest takes further reads with different parameters); a length byte
503/// pointing past the response must not panic. `None` for non-UTF-8 bytes.
504fn parse_codename(response: &[u8; 16]) -> Option<&str> {
505    let end = 3usize.saturating_add(usize::from(response[2]));
506    let raw = response.get(3..end.min(response.len()))?;
507    str::from_utf8(raw).ok()
508}
509
510/// Indicates which notifications are enabled and thus sent by the receiver.
511///
512/// This information can be queried using [`Receiver::get_notification_state`].
513#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Builder)]
514#[cfg_attr(feature = "serde", derive(serde::Serialize))]
515#[non_exhaustive]
516pub struct NotificationState {
517    /// Whether the receiver sends device arrival/removal notifications.
518    pub wireless_notifications: bool,
519}
520
521/// Represents information about a paired device.
522///
523/// This information can be queried using
524/// [`Receiver::get_device_pairing_information`].
525#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
526#[cfg_attr(feature = "serde", derive(serde::Serialize))]
527#[non_exhaustive]
528pub struct DevicePairingInformation {
529    /// Wireless product ID of the paired device.
530    pub wpid: u16,
531    /// Device kind reported by the receiver.
532    pub kind: DeviceKind,
533    /// Whether the link is encrypted.
534    pub encrypted: bool,
535    /// Whether the device is currently online.
536    pub online: bool,
537    /// Device unit ID.
538    pub unit_id: [u8; 4],
539}
540
541/// Represents the kind of a device paired to a Bolt receiver.
542#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, FromPrimitive)]
543#[cfg_attr(feature = "serde", derive(serde::Serialize))]
544#[non_exhaustive]
545#[repr(u8)]
546pub enum DeviceKind {
547    /// Unknown device kind — also the fold target for values this crate
548    /// does not model (kind is identity-only and must never drop an event).
549    #[num_enum(default)]
550    Unknown = 0x00,
551    /// Keyboard device.
552    Keyboard = 0x01,
553    /// Mouse device.
554    Mouse = 0x02,
555    /// Numeric keypad device.
556    Numpad = 0x03,
557    /// Presenter device.
558    Presenter = 0x04,
559    /// Remote-control device.
560    Remote = 0x07,
561    /// Trackball device.
562    Trackball = 0x08,
563    /// Touchpad device.
564    Touchpad = 0x09,
565    /// Tablet device.
566    Tablet = 0x0a,
567    /// Gamepad device.
568    Gamepad = 0x0b,
569    /// Joystick device.
570    Joystick = 0x0c,
571    /// Headset device.
572    Headset = 0x0d,
573}
574
575/// Represents an event emitted by the receiver.
576///
577/// You can listen to these events using [`Receiver::listen`]. Only enabled
578/// notifications as indicated by [`Receiver::get_notification_state`] are
579/// emitted.
580#[derive(Clone, PartialEq, Eq, Hash, Debug)]
581#[cfg_attr(feature = "serde", derive(serde::Serialize))]
582#[non_exhaustive]
583pub enum Event {
584    /// Is emitted whenever a device connects to or disconnects from the
585    /// receiver, but only if [`NotificationState::wireless_notifications`] is
586    /// enabled.
587    ///
588    /// Can be triggered for all paired devices using
589    /// [`Receiver::trigger_device_arrival`] to allow easy device enumeration.
590    ///
591    /// [`Receiver::collect_paired_devices`] implements a simple mechanism to
592    /// collect all paired devices.
593    DeviceConnection(DeviceConnection),
594
595    /// Is emitted whenever the device discovery status changes.
596    DeviceDiscoveryStatus {
597        /// Whether discovery mode is enabled.
598        discovery_enabled: bool,
599    },
600
601    /// Is emitted many times for every device discovered using
602    /// [`Receiver::discover_devices`].
603    ///
604    /// This event contains device details, including its address required to
605    /// start pairing. The [`Event::DeviceDiscoveryDeviceName`] event will also
606    /// be emitted and contains the device name.
607    DeviceDiscoveryDeviceDetails {
608        /// The incrementing event counter. This can be used to map
609        /// [`Event::DeviceDiscoveryDeviceDetails`] and
610        /// [`Event::DeviceDiscoveryDeviceName`] events.
611        counter: u16,
612
613        /// Device kind reported by discovery.
614        kind: DeviceKind,
615        /// Wireless product ID of the discovered device.
616        wpid: u16,
617
618        /// The address of the device required to pair it using
619        /// [`Receiver::pair_device`].
620        ///
621        /// This can also be used as the unique device identifier when
622        /// collecting discovered devices.
623        address: [u8; 6],
624
625        /// The authentication type(s) the device supports. Unfortunately, there
626        /// is not much information about this value and whether it is a
627        /// single value or a bitfield.
628        authentication: u8,
629    },
630
631    /// Is emitted many times for every device discovered using
632    /// [`Receiver::discover_devices`].
633    ///
634    /// This event only contains the device name. Device details will be
635    /// provided using the [`Event::DeviceDiscoveryDeviceDetails`] event.
636    DeviceDiscoveryDeviceName {
637        /// The incrementing event counter. This can be used to map
638        /// [`Event::DeviceDiscoveryDeviceDetails`] and
639        /// [`Event::DeviceDiscoveryDeviceName`] events.
640        counter: u16,
641
642        /// Discovered device name.
643        name: String,
644    },
645
646    /// Is emitted whenever the status of a pairing process changes.
647    PairingStatus {
648        /// BTLE address of the device being paired.
649        device_address: [u8; 6],
650        /// Optional pairing error reported by the receiver.
651        pairing_error: Option<PairingError>,
652
653        /// The receiver slot the newly paired device was paired to. This can be
654        /// used as the device index for subsequent operations.
655        slot: Option<u8>,
656    },
657
658    /// Is emitted once the receiver requests a passkey to be entered on a
659    /// device that should be paired to it.
660    PairingPasskeyRequest {
661        /// BTLE address of the device being paired.
662        device_address: [u8; 6],
663
664        /// The passkey the user has to enter in order to pair the device.
665        ///
666        /// Depending on the device and authentication type, this value has
667        /// different implications.
668        ///
669        /// For mice, this value will be a valid 6-digit number. After parsing
670        /// this into an integer, the (least significant) bits represent
671        /// the sequence of mouse presses (`0` = left, `1` = right) the
672        /// user has to perform, with an additional press of both mouse
673        /// buttons simultaneously.
674        ///
675        /// The amount of bits significant to this equals to the `entropy`
676        /// passed to [`Receiver::pair_device`].
677        passkey: String,
678    },
679
680    /// Is emitted for every keypress a user performs while entering a pairing
681    /// passkey.
682    PairingPasskeyPressed {
683        /// BTLE address of the device being paired.
684        device_address: [u8; 6],
685
686        /// The type of the keypress the user performed.
687        ///
688        /// Every passkey sequence starts with an event where this value is set
689        /// to [`PairingPasskeyPressType::Initialization`]. Each time the user
690        /// presses a key, an event with a press type of
691        /// [`PairingPasskeyPressType::Keypress`] is emitted. Once the user
692        /// submits their passkey, this value will be
693        /// [`PairingPasskeyPressType::Submit`].
694        press_type: PairingPasskeyPressType,
695    },
696}
697
698/// Represents a device connected to a Bolt receiver.
699///
700/// This information is emitted by the [`Event::DeviceConnection`] event and can
701/// be conveniently collected using [`Receiver::collect_paired_devices`].
702#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
703#[cfg_attr(feature = "serde", derive(serde::Serialize))]
704#[non_exhaustive]
705pub struct DeviceConnection {
706    /// Slot index (1-based) of the device.
707    pub index: u8,
708    /// Device kind reported by the receiver.
709    pub kind: DeviceKind,
710    /// Whether the link is encrypted.
711    pub encrypted: bool,
712    /// Whether the device is currently online.
713    pub online: bool,
714    /// Wireless product ID of the device.
715    pub wpid: u16,
716}
717
718/// Represents an error during device pairing.
719///
720/// This is reported by the [`Event::PairingStatus`] event.
721#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, FromPrimitive, IntoPrimitive)]
722#[cfg_attr(feature = "serde", derive(serde::Serialize))]
723#[non_exhaustive]
724#[repr(u8)]
725pub enum PairingError {
726    /// Device timed out during pairing.
727    DeviceTimeout = 0x01,
728    /// Pairing failed.
729    Failed = 0x02,
730    /// An error code this crate does not model; carries the raw byte.
731    #[num_enum(catch_all)]
732    Other(u8),
733}
734
735/// Represents the type of a single passkey press.
736///
737/// This is reported by the [`Event::PairingPasskeyPressed`] event, which also
738/// includes some further information about the context of these values.
739#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, FromPrimitive, IntoPrimitive)]
740#[cfg_attr(feature = "serde", derive(serde::Serialize))]
741#[non_exhaustive]
742#[repr(u8)]
743pub enum PairingPasskeyPressType {
744    /// Passkey entry has started.
745    Initialization = 0x00,
746    /// A passkey keypress was entered.
747    Keypress = 0x01,
748    /// Passkey entry was submitted.
749    Submit = 0x04,
750    /// A press type this crate does not model; carries the raw byte.
751    #[num_enum(catch_all)]
752    Other(u8),
753}
754
755#[cfg(test)]
756mod tests {
757    use super::*;
758
759    #[test]
760    fn discovery_name_with_oversized_length_is_dropped() {
761        let mut payload = [0u8; 17];
762        payload[3] = 200;
763
764        assert_eq!(parse_discovery_name(&payload), None);
765    }
766
767    #[test]
768    fn discovery_name_within_bounds_parses() {
769        let mut payload = [0u8; 17];
770        payload[0] = 7;
771        payload[3] = 4;
772        payload[4..8].copy_from_slice(b"Casa");
773
774        assert_eq!(parse_discovery_name(&payload), Some((7, "Casa")));
775    }
776
777    #[test]
778    fn discovery_name_rejects_invalid_utf8() {
779        let mut payload = [0u8; 17];
780        payload[3] = 2;
781        payload[4] = 0xff;
782        payload[5] = 0xfe;
783
784        assert_eq!(parse_discovery_name(&payload), None);
785    }
786
787    #[test]
788    fn codename_with_oversized_length_clamps_to_available_chunk() {
789        let mut response = [0u8; 16];
790        response[2] = 200;
791        response[3..16].copy_from_slice(b"MX Anywhere 3");
792
793        assert_eq!(parse_codename(&response), Some("MX Anywhere 3"));
794    }
795
796    #[test]
797    fn codename_within_bounds_parses() {
798        let mut response = [0u8; 16];
799        response[2] = 5;
800        response[3..8].copy_from_slice(b"Casa!");
801
802        assert_eq!(parse_codename(&response), Some("Casa!"));
803    }
804
805    #[test]
806    fn codename_rejects_invalid_utf8() {
807        let mut response = [0u8; 16];
808        response[2] = 2;
809        response[3] = 0xff;
810        response[4] = 0xfe;
811
812        assert_eq!(parse_codename(&response), None);
813    }
814}