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