1use std::{collections::HashMap, sync::Arc};
27
28use hidpp::{
29 channel::{HidppChannel, HidppMessage},
30 receiver::{self, Receiver},
31};
32use serde::{Deserialize, Serialize};
33use thiserror::Error;
34use tokio::sync::mpsc;
35use tracing::{debug, trace, warn};
36
37pub use hidpp::receiver::bolt::DeviceKind as BoltDeviceKind;
38
39use crate::transport::{enumerate_hidpp_devices, open_hidpp_channel};
40
41const RECEIVER_INDEX: u8 = 0xff;
43
44mod reg {
46 pub const NOTIFICATIONS: u8 = 0x00;
48 pub const UNIFYING_PAIRING: u8 = 0xb2;
50 pub const BOLT_DISCOVERY: u8 = 0xc0;
52 pub const BOLT_PAIRING: u8 = 0xc1;
54}
55
56mod notif {
58 pub const DEVICE_CONNECTION: u8 = 0x41;
59 pub const UNIFYING_LOCK: u8 = 0x4a;
60 pub const PASSKEY_REQUEST: u8 = 0x4d;
61 pub const DEVICE_DISCOVERY: u8 = 0x4f;
62 pub const DISCOVERY_STATUS: u8 = 0x53;
63 pub const PAIRING_STATUS: u8 = 0x54;
64}
65
66const NOTIF_FLAGS: [u8; 3] = [0x00, 0x09, 0x00];
69
70#[derive(Clone, Copy, PartialEq, Eq, Debug)]
72pub enum ReceiverFamily {
73 Bolt,
74 Unifying,
75}
76
77fn family_for(product_id: u16) -> Option<ReceiverFamily> {
78 if crate::BOLT_PIDS.contains(&product_id) {
79 Some(ReceiverFamily::Bolt)
80 } else if crate::UNIFYING_PIDS.contains(&product_id) {
81 Some(ReceiverFamily::Unifying)
82 } else {
83 None
84 }
85}
86
87#[derive(Clone, Debug)]
89pub struct PairingReceiver {
90 pub uid: Option<String>,
92 pub family: ReceiverFamily,
93 pub product_id: u16,
94}
95
96#[derive(Clone, Debug, Serialize, Deserialize)]
102pub enum ReceiverSelector {
103 First,
105 BoltUid(String),
107}
108
109#[derive(Clone, Debug)]
111pub struct DiscoveredDevice {
112 pub address: [u8; 6],
114 pub authentication: u8,
116 pub kind: BoltDeviceKind,
117 pub name: String,
118}
119
120impl DiscoveredDevice {
121 #[must_use]
124 pub fn passkey_on_keyboard(&self) -> bool {
125 self.authentication & 0x01 != 0
126 }
127
128 fn entropy(&self) -> u8 {
130 if self.kind == BoltDeviceKind::Keyboard {
131 20
132 } else {
133 10
134 }
135 }
136}
137
138#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
140pub enum Click {
141 Left,
142 Right,
143}
144
145#[derive(Clone, Debug, Serialize, Deserialize)]
152pub enum PasskeyMethod {
153 Keyboard(String),
155 Pointer { passkey: String, clicks: Vec<Click> },
158}
159
160fn passkey_to_clicks(passkey: &str) -> Vec<Click> {
162 let value: u32 = passkey.trim().parse().unwrap_or(0);
163 (0..10)
164 .rev()
165 .map(|bit| {
166 if value & (1 << bit) != 0 {
167 Click::Right
168 } else {
169 Click::Left
170 }
171 })
172 .collect()
173}
174
175#[derive(Clone, Debug)]
177pub enum PairingEvent {
178 Searching,
180 DeviceFound(DiscoveredDevice),
182 Passkey(PasskeyMethod),
184 Paired { slot: u8 },
186 Failed(PairingError),
188}
189
190#[derive(Clone, Debug)]
192pub enum PairingCommand {
193 Pair(DiscoveredDevice),
195 Cancel,
197}
198
199#[derive(Clone, Debug, Error)]
201pub enum PairingError {
202 #[error("HID transport error: {0}")]
203 Hid(String),
204 #[error("no supported pairing-capable receiver found")]
205 ReceiverNotFound,
206 #[error("receiver register access failed: {0}")]
207 Register(String),
208 #[error("pairing timed out")]
209 Timeout,
210 #[error("receiver reported pairing error {0:#04x}")]
211 Device(u8),
212 #[error("pairing was cancelled")]
213 Cancelled,
214}
215
216impl From<async_hid::HidError> for PairingError {
217 fn from(e: async_hid::HidError) -> Self {
218 PairingError::Hid(e.to_string())
219 }
220}
221
222pub async fn list_pairing_receivers() -> Result<Vec<PairingReceiver>, PairingError> {
224 let mut out = Vec::new();
225 for dev in enumerate_hidpp_devices().await? {
226 let Some((_, channel)) = open_hidpp_channel(dev).await? else {
227 continue;
228 };
229 let Some(family) = family_for(channel.product_id) else {
230 continue;
231 };
232 let uid = match family {
233 ReceiverFamily::Bolt => read_bolt_uid(&channel).await,
234 ReceiverFamily::Unifying => None,
235 };
236 out.push(PairingReceiver {
237 uid,
238 family,
239 product_id: channel.product_id,
240 });
241 }
242 Ok(out)
243}
244
245async fn read_bolt_uid(channel: &Arc<HidppChannel>) -> Option<String> {
247 let Some(Receiver::Bolt(bolt)) = receiver::detect(Arc::clone(channel)) else {
248 return None;
249 };
250 bolt.get_unique_id().await.ok()
251}
252
253async fn open_receiver(
255 target: &ReceiverSelector,
256) -> Result<(Arc<HidppChannel>, ReceiverFamily), PairingError> {
257 for dev in enumerate_hidpp_devices().await? {
258 let Some((_, channel)) = open_hidpp_channel(dev).await? else {
259 continue;
260 };
261 let Some(family) = family_for(channel.product_id) else {
262 continue;
263 };
264 match target {
265 ReceiverSelector::First => return Ok((channel, family)),
266 ReceiverSelector::BoltUid(want) => {
267 if family == ReceiverFamily::Bolt
268 && read_bolt_uid(&channel)
269 .await
270 .is_some_and(|uid| uid.eq_ignore_ascii_case(want))
271 {
272 return Ok((channel, family));
273 }
274 }
275 }
276 }
277 Err(PairingError::ReceiverNotFound)
278}
279
280fn decode(msg: &HidppMessage) -> (u8, u8, [u8; 17]) {
284 let mut payload = [0u8; 17];
285 match msg {
286 HidppMessage::Short(d) => {
287 payload[..4].copy_from_slice(&d[2..6]);
288 (d[0], d[1], payload)
289 }
290 HidppMessage::Long(d) => {
291 payload.copy_from_slice(&d[2..19]);
292 (d[0], d[1], payload)
293 }
294 }
295}
296
297#[derive(Clone, Debug, PartialEq, Eq)]
299enum Notification {
300 DiscoveryInfo {
302 counter: u16,
303 kind: u8,
304 address: [u8; 6],
305 authentication: u8,
306 },
307 DiscoveryName { counter: u16, name: String },
309 PairingSucceeded { slot: u8 },
311 PairingError(u8),
313 Passkey(String),
315 Connected { slot: u8, established: bool },
317 UnifyingLock { open: bool, error: u8 },
319}
320
321fn parse_notification(sub_id: u8, device_index: u8, p: [u8; 17]) -> Option<Notification> {
323 match sub_id {
324 notif::DEVICE_CONNECTION => Some(Notification::Connected {
325 slot: device_index,
326 established: p[1] & (1 << 6) == 0,
328 }),
329 notif::DEVICE_DISCOVERY => {
330 let counter = u16::from(p[0]) + u16::from(p[1]) * 256;
331 match p[2] {
332 0 => {
333 let mut address = [0u8; 6];
334 address.copy_from_slice(&p[7..13]);
335 Some(Notification::DiscoveryInfo {
336 counter,
337 kind: p[4],
338 address,
339 authentication: p[15],
340 })
341 }
342 1 => {
343 let len = usize::from(p[3]).min(p.len() - 4);
344 let name = String::from_utf8_lossy(&p[4..4 + len]).into_owned();
345 Some(Notification::DiscoveryName { counter, name })
346 }
347 _ => None,
348 }
349 }
350 notif::DISCOVERY_STATUS => {
351 let error = p[1];
352 if error != 0 {
353 Some(Notification::PairingError(error))
354 } else {
355 None
356 }
357 }
358 notif::PAIRING_STATUS => {
359 let error = p[1];
360 if error != 0 {
361 Some(Notification::PairingError(error))
362 } else if p[0] == 0x02 {
363 Some(Notification::PairingSucceeded { slot: p[8] })
365 } else {
366 None
367 }
368 }
369 notif::PASSKEY_REQUEST => {
370 let passkey = String::from_utf8_lossy(&p[1..7]).into_owned();
371 Some(Notification::Passkey(passkey))
372 }
373 notif::UNIFYING_LOCK => Some(Notification::UnifyingLock {
374 open: p[0] & 0x01 != 0,
375 error: p[1],
376 }),
377 _ => None,
378 }
379}
380
381fn subscribe(channel: &HidppChannel) -> (u32, mpsc::UnboundedReceiver<HidppMessage>) {
384 let (tx, rx) = mpsc::unbounded_channel();
385 let hdl = channel.add_msg_listener(move |msg, matched| {
386 if !matched {
388 let _ = tx.send(msg);
389 }
390 });
391 (hdl, rx)
392}
393
394const SESSION_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(90);
396const DISCOVERY_TIMEOUT: u8 = 30;
398
399pub async fn run_pairing(
407 target: ReceiverSelector,
408 mut commands: mpsc::UnboundedReceiver<PairingCommand>,
409 events: mpsc::UnboundedSender<PairingEvent>,
410) -> Result<(), PairingError> {
411 let (channel, family) = open_receiver(&target).await?;
412 let (listener, mut notifications) = subscribe(&channel);
413
414 let result = drive(&channel, family, &mut commands, &mut notifications, &events).await;
415
416 channel.remove_msg_listener(listener);
417 let _ = channel
419 .write_register(RECEIVER_INDEX, reg::NOTIFICATIONS, [0, 0, 0])
420 .await;
421
422 if let Err(ref e) = result {
423 let _ = events.send(PairingEvent::Failed(e.clone()));
424 }
425 result
426}
427
428async fn drive(
430 channel: &HidppChannel,
431 family: ReceiverFamily,
432 commands: &mut mpsc::UnboundedReceiver<PairingCommand>,
433 notifications: &mut mpsc::UnboundedReceiver<HidppMessage>,
434 events: &mpsc::UnboundedSender<PairingEvent>,
435) -> Result<(), PairingError> {
436 write_register(channel, reg::NOTIFICATIONS, NOTIF_FLAGS).await?;
437
438 match family {
439 ReceiverFamily::Bolt => {
440 write_register(
441 channel,
442 reg::BOLT_DISCOVERY,
443 [DISCOVERY_TIMEOUT, 0x01, 0x00],
444 )
445 .await?;
446 }
447 ReceiverFamily::Unifying => {
448 write_register(
449 channel,
450 reg::UNIFYING_PAIRING,
451 [0x01, 0x00, DISCOVERY_TIMEOUT],
452 )
453 .await?;
454 }
455 }
456 let _ = events.send(PairingEvent::Searching);
457
458 let mut partial: HashMap<u16, PartialDevice> = HashMap::new();
460 let mut pairing_auth: Option<u8> = None;
462 let deadline = tokio::time::sleep(SESSION_TIMEOUT);
463 tokio::pin!(deadline);
464
465 loop {
466 tokio::select! {
467 () = &mut deadline => return Err(PairingError::Timeout),
468
469 cmd = commands.recv() => match cmd {
470 Some(PairingCommand::Pair(device)) => {
471 pairing_auth = Some(device.authentication);
472 pair_bolt_device(channel, &device).await?;
473 }
474 Some(PairingCommand::Cancel) | None => {
475 cancel(channel, family).await;
476 return Err(PairingError::Cancelled);
477 }
478 },
479
480 msg = notifications.recv() => {
481 let Some(msg) = msg else {
482 return Err(PairingError::Hid("receiver channel closed".into()));
483 };
484 let (device_index, sub_id, payload) = decode(&msg);
485 trace!(sub_id = format_args!("{sub_id:#04x}"), ?payload, "pairing notification");
488 let Some(note) = parse_notification(sub_id, device_index, payload) else {
489 continue;
490 };
491 match note {
492 Notification::DiscoveryInfo { counter, kind, address, authentication } => {
493 let entry = partial.entry(counter).or_default();
494 entry.kind = Some(kind);
495 entry.address = Some(address);
496 entry.authentication = Some(authentication);
497 if let Some(device) = entry.build() {
498 let _ = events.send(PairingEvent::DeviceFound(device));
499 }
500 }
501 Notification::DiscoveryName { counter, name } => {
502 let entry = partial.entry(counter).or_default();
503 entry.name = Some(name);
504 if let Some(device) = entry.build() {
505 let _ = events.send(PairingEvent::DeviceFound(device));
506 }
507 }
508 Notification::Passkey(passkey) => {
509 let method = match pairing_auth {
510 Some(auth) if auth & 0x01 != 0 => PasskeyMethod::Keyboard(passkey),
511 _ => PasskeyMethod::Pointer {
512 clicks: passkey_to_clicks(&passkey),
513 passkey,
514 },
515 };
516 let _ = events.send(PairingEvent::Passkey(method));
517 }
518 Notification::PairingSucceeded { slot } => {
519 let _ = events.send(PairingEvent::Paired { slot });
520 return Ok(());
521 }
522 Notification::PairingError(code) => return Err(PairingError::Device(code)),
523 Notification::Connected { slot, established } if family == ReceiverFamily::Unifying => {
524 if established {
525 let _ = events.send(PairingEvent::Paired { slot });
526 return Ok(());
527 }
528 }
529 Notification::Connected { .. } => {}
530 Notification::UnifyingLock { open, error } => {
531 if error != 0 {
532 return Err(PairingError::Device(error));
533 }
534 if !open {
535 return Err(PairingError::Timeout);
537 }
538 }
539 }
540 }
541 }
542 }
543}
544
545#[derive(Default)]
547struct PartialDevice {
548 kind: Option<u8>,
549 address: Option<[u8; 6]>,
550 authentication: Option<u8>,
551 name: Option<String>,
552 emitted: bool,
553}
554
555impl PartialDevice {
556 fn build(&mut self) -> Option<DiscoveredDevice> {
558 if self.emitted {
559 return None;
560 }
561 let (kind, address, authentication, name) = (
562 self.kind?,
563 self.address?,
564 self.authentication?,
565 self.name.clone()?,
566 );
567 self.emitted = true;
568 Some(DiscoveredDevice {
569 address,
570 authentication,
571 kind: BoltDeviceKind::try_from(kind & 0x0f).unwrap_or(BoltDeviceKind::Unknown),
572 name,
573 })
574 }
575}
576
577async fn pair_bolt_device(
579 channel: &HidppChannel,
580 device: &DiscoveredDevice,
581) -> Result<(), PairingError> {
582 let mut payload = [0u8; 16];
583 payload[0] = 0x01; payload[1] = 0x00; payload[2..8].copy_from_slice(&device.address);
586 payload[8] = device.authentication;
587 payload[9] = device.entropy();
588 write_long_register(channel, reg::BOLT_PAIRING, payload).await
589}
590
591async fn cancel(channel: &HidppChannel, family: ReceiverFamily) {
593 let res = match family {
594 ReceiverFamily::Bolt => {
595 write_register(
596 channel,
597 reg::BOLT_DISCOVERY,
598 [DISCOVERY_TIMEOUT, 0x02, 0x00],
599 )
600 .await
601 }
602 ReceiverFamily::Unifying => {
603 write_register(channel, reg::UNIFYING_PAIRING, [0x02, 0x00, 0x00]).await
604 }
605 };
606 if let Err(e) = res {
607 debug!(?e, "cancel write failed");
608 }
609}
610
611pub async fn unpair(target: ReceiverSelector, slot: u8) -> Result<(), PairingError> {
613 let (channel, family) = open_receiver(&target).await?;
614 match family {
615 ReceiverFamily::Bolt => {
616 let mut payload = [0u8; 16];
617 payload[0] = 0x03; payload[1] = slot;
619 write_long_register(&channel, reg::BOLT_PAIRING, payload).await
620 }
621 ReceiverFamily::Unifying => {
622 write_register(&channel, reg::UNIFYING_PAIRING, [0x03, slot, 0x00]).await
623 }
624 }
625}
626
627async fn write_register(
628 channel: &HidppChannel,
629 address: u8,
630 payload: [u8; 3],
631) -> Result<(), PairingError> {
632 channel
633 .write_register(RECEIVER_INDEX, address, payload)
634 .await
635 .map_err(|e| {
636 warn!(
637 register = format_args!("{address:#04x}"),
638 ?e,
639 "register write failed"
640 );
641 PairingError::Register(format!("{e}"))
642 })
643}
644
645async fn write_long_register(
646 channel: &HidppChannel,
647 address: u8,
648 payload: [u8; 16],
649) -> Result<(), PairingError> {
650 channel
651 .write_long_register(RECEIVER_INDEX, address, payload)
652 .await
653 .map_err(|e| {
654 warn!(
655 register = format_args!("{address:#04x}"),
656 ?e,
657 "long register write failed"
658 );
659 PairingError::Register(format!("{e}"))
660 })
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666
667 fn long(sub_id: u8, device_index: u8, p: [u8; 17]) -> HidppMessage {
669 let mut d = [0u8; 19];
670 d[0] = device_index;
671 d[1] = sub_id;
672 d[2..19].copy_from_slice(&p);
673 HidppMessage::Long(d)
674 }
675
676 #[test]
677 fn decode_maps_long_payload_to_address_first() {
678 let msg = long(notif::DEVICE_DISCOVERY, 0xff, {
679 let mut p = [0u8; 17];
680 p[0] = 0x07; p[1] = 0x00; p
683 });
684 let (idx, sub, payload) = decode(&msg);
685 assert_eq!(idx, 0xff);
686 assert_eq!(sub, notif::DEVICE_DISCOVERY);
687 assert_eq!(payload[0], 0x07);
688 assert_eq!(payload[1], 0x00);
689 }
690
691 #[test]
692 fn parses_discovery_info_frame() {
693 let mut p = [0u8; 17];
694 p[0] = 0x05; p[1] = 0x00; p[2] = 0x00; p[4] = 0x02; p[7..13].copy_from_slice(&[0xde, 0xad, 0xbe, 0xef, 0x01, 0x02]);
699 p[15] = 0x01; assert_eq!(
701 parse_notification(notif::DEVICE_DISCOVERY, 0xff, p),
702 Some(Notification::DiscoveryInfo {
703 counter: 5,
704 kind: 0x02,
705 address: [0xde, 0xad, 0xbe, 0xef, 0x01, 0x02],
706 authentication: 0x01,
707 })
708 );
709 }
710
711 #[test]
712 fn parses_discovery_name_frame() {
713 let mut p = [0u8; 17];
714 p[0] = 0x05;
715 p[1] = 0x00;
716 p[2] = 0x01; p[3] = 0x03; p[4..7].copy_from_slice(b"MX3");
719 assert_eq!(
720 parse_notification(notif::DEVICE_DISCOVERY, 0xff, p),
721 Some(Notification::DiscoveryName {
722 counter: 5,
723 name: "MX3".to_string(),
724 })
725 );
726 }
727
728 #[test]
729 fn parses_pairing_success_with_slot() {
730 let mut p = [0u8; 17];
731 p[0] = 0x02; p[1] = 0x00; p[8] = 0x03; assert_eq!(
735 parse_notification(notif::PAIRING_STATUS, 0xff, p),
736 Some(Notification::PairingSucceeded { slot: 3 })
737 );
738 }
739
740 #[test]
741 fn parses_pairing_error() {
742 let mut p = [0u8; 17];
743 p[0] = 0x00;
744 p[1] = 0x01; assert_eq!(
746 parse_notification(notif::PAIRING_STATUS, 0xff, p),
747 Some(Notification::PairingError(0x01))
748 );
749 }
750
751 #[test]
752 fn parses_passkey_digits() {
753 let mut p = [0u8; 17];
754 p[1..7].copy_from_slice(b"123456");
755 assert_eq!(
756 parse_notification(notif::PASSKEY_REQUEST, 0xff, p),
757 Some(Notification::Passkey("123456".to_string()))
758 );
759 }
760
761 #[test]
762 fn parses_unifying_lock() {
763 let mut p = [0u8; 17];
764 p[0] = 0x01; assert_eq!(
766 parse_notification(notif::UNIFYING_LOCK, 0xff, p),
767 Some(Notification::UnifyingLock {
768 open: true,
769 error: 0
770 })
771 );
772 }
773
774 #[test]
775 fn passkey_clicks_are_msb_first_10_bits() {
776 assert_eq!(
778 passkey_to_clicks("5"),
779 vec![
780 Click::Left,
781 Click::Left,
782 Click::Left,
783 Click::Left,
784 Click::Left,
785 Click::Left,
786 Click::Left,
787 Click::Right,
788 Click::Left,
789 Click::Right,
790 ]
791 );
792 }
793}