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}