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};
36
37pub use hidpp::receiver::bolt::DeviceKind as BoltDeviceKind;
38
39use crate::transport::{enumerate_hidpp_devices, open_hidpp_channel};
40
41mod notification;
42mod registers;
43
44use notification::{Notification, decode, parse_notification, subscribe};
45use registers::{
46 BOLT_DISCOVERY, BOLT_PAIRING, NOTIFICATION_FLAGS, NOTIFICATIONS, UNIFYING_PAIRING,
47 write_long_register, write_register,
48};
49
50const RECEIVER_INDEX: u8 = 0xff;
52
53#[derive(Clone, Copy, PartialEq, Eq, Debug)]
55pub enum ReceiverFamily {
56 Bolt,
58 Unifying,
60}
61
62#[derive(Clone, Copy, PartialEq, Eq, Debug)]
63enum PairingPhase {
64 BoltDiscovery,
65 BoltPairing,
66 UnifyingPairing,
67}
68
69impl From<ReceiverFamily> for PairingPhase {
70 fn from(family: ReceiverFamily) -> Self {
71 match family {
72 ReceiverFamily::Bolt => Self::BoltDiscovery,
73 ReceiverFamily::Unifying => Self::UnifyingPairing,
74 }
75 }
76}
77
78fn family_for(product_id: u16) -> Option<ReceiverFamily> {
79 if crate::BOLT_PIDS.contains(&product_id) {
80 Some(ReceiverFamily::Bolt)
81 } else if crate::UNIFYING_PIDS.contains(&product_id) {
82 Some(ReceiverFamily::Unifying)
83 } else {
84 None
85 }
86}
87
88#[derive(Clone, Debug)]
90pub struct PairingReceiver {
91 pub uid: Option<String>,
93 pub family: ReceiverFamily,
95 pub product_id: u16,
97}
98
99#[derive(Clone, Debug, Serialize, Deserialize)]
105pub enum ReceiverSelector {
106 First,
108 BoltUid(String),
110}
111
112#[derive(Clone, Debug)]
114pub struct DiscoveredDevice {
115 pub address: [u8; 6],
117 pub authentication: u8,
119 pub kind: BoltDeviceKind,
121 pub name: String,
123}
124
125impl DiscoveredDevice {
126 #[must_use]
129 pub fn passkey_on_keyboard(&self) -> bool {
130 self.authentication & 0x01 != 0
131 }
132
133 fn entropy(&self) -> u8 {
135 if self.kind == BoltDeviceKind::Keyboard {
136 20
137 } else {
138 10
139 }
140 }
141}
142
143#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
145pub enum Click {
146 Left,
148 Right,
150}
151
152#[derive(Clone, Debug, Serialize, Deserialize)]
159pub enum PasskeyMethod {
160 Keyboard(String),
162 Pointer {
165 passkey: String,
167 clicks: Vec<Click>,
169 },
170}
171
172fn passkey_to_clicks(value: u32) -> Vec<Click> {
174 (0..10)
175 .rev()
176 .map(|bit| {
177 if value & (1 << bit) != 0 {
178 Click::Right
179 } else {
180 Click::Left
181 }
182 })
183 .collect()
184}
185
186#[derive(Clone, Debug)]
188pub enum PairingEvent {
189 Searching,
191 DeviceFound(DiscoveredDevice),
193 Passkey(PasskeyMethod),
195 Paired {
197 slot: u8,
199 },
200 Failed(PairingError),
202}
203
204#[derive(Clone, Debug)]
206pub enum PairingCommand {
207 Pair(DiscoveredDevice),
209 Cancel,
211}
212
213#[derive(Clone, Debug, Error)]
215pub enum PairingError {
216 #[error("HID transport error: {0}")]
218 Hid(String),
219 #[error("no supported pairing-capable receiver found")]
221 ReceiverNotFound,
222 #[error("receiver register access failed: {0}")]
224 Register(String),
225 #[error("pairing timed out")]
227 Timeout,
228 #[error("receiver reported pairing error {0:#04x}")]
230 Device(u8),
231 #[error("pairing was cancelled")]
233 Cancelled,
234 #[error("malformed pairing notification ({0})")]
237 MalformedNotification(&'static str),
238}
239
240impl From<async_hid::HidError> for PairingError {
241 fn from(e: async_hid::HidError) -> Self {
242 PairingError::Hid(e.to_string())
243 }
244}
245
246pub async fn list_pairing_receivers() -> Result<Vec<PairingReceiver>, PairingError> {
248 let mut out = Vec::new();
249 for dev in enumerate_hidpp_devices().await? {
250 let Some((_, channel)) = open_hidpp_channel(dev).await? else {
251 continue;
252 };
253 let Some(family) = family_for(channel.product_id) else {
254 continue;
255 };
256 let uid = match family {
257 ReceiverFamily::Bolt => read_bolt_uid(&channel).await,
258 ReceiverFamily::Unifying => None,
259 };
260 out.push(PairingReceiver {
261 uid,
262 family,
263 product_id: channel.product_id,
264 });
265 }
266 Ok(out)
267}
268
269async fn read_bolt_uid(channel: &Arc<HidppChannel>) -> Option<String> {
271 let Some(Receiver::Bolt(bolt)) = receiver::detect(Arc::clone(channel)) else {
272 return None;
273 };
274 bolt.get_unique_id().await.ok()
275}
276
277async fn open_receiver(
279 target: &ReceiverSelector,
280) -> Result<(Arc<HidppChannel>, ReceiverFamily), PairingError> {
281 for dev in enumerate_hidpp_devices().await? {
282 let Some((_, channel)) = open_hidpp_channel(dev).await? else {
283 continue;
284 };
285 let Some(family) = family_for(channel.product_id) else {
286 continue;
287 };
288 match target {
289 ReceiverSelector::First => return Ok((channel, family)),
290 ReceiverSelector::BoltUid(want) => {
291 if family == ReceiverFamily::Bolt
292 && read_bolt_uid(&channel)
293 .await
294 .is_some_and(|uid| uid.eq_ignore_ascii_case(want))
295 {
296 return Ok((channel, family));
297 }
298 }
299 }
300 }
301 Err(PairingError::ReceiverNotFound)
302}
303
304const SESSION_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(90);
306const DISCOVERY_TIMEOUT: u8 = 30;
308
309pub async fn run_pairing(
317 target: ReceiverSelector,
318 mut commands: mpsc::UnboundedReceiver<PairingCommand>,
319 events: mpsc::UnboundedSender<PairingEvent>,
320) -> Result<(), PairingError> {
321 let (channel, family) = match open_receiver(&target).await {
322 Ok(receiver) => receiver,
323 Err(e) => {
324 let _ = events.send(PairingEvent::Failed(e.clone()));
325 return Err(e);
326 }
327 };
328 let (listener, mut notifications) = subscribe(&channel);
329
330 let result = run_session(&channel, family, &mut commands, &mut notifications, &events).await;
331
332 drop(listener);
333 let _ = channel
335 .write_register(RECEIVER_INDEX, NOTIFICATIONS, [0, 0, 0])
336 .await;
337
338 if let Err(ref e) = result {
339 let _ = events.send(PairingEvent::Failed(e.clone()));
340 }
341 result
342}
343
344async fn run_session(
346 channel: &HidppChannel,
347 family: ReceiverFamily,
348 commands: &mut mpsc::UnboundedReceiver<PairingCommand>,
349 notifications: &mut mpsc::UnboundedReceiver<HidppMessage>,
350 events: &mpsc::UnboundedSender<PairingEvent>,
351) -> Result<(), PairingError> {
352 let mut phase = PairingPhase::from(family);
353 let result = drive(channel, family, &mut phase, commands, notifications, events).await;
354 if result.is_err() {
355 cancel(channel, phase).await;
356 }
357 result
358}
359
360async fn drive(
362 channel: &HidppChannel,
363 family: ReceiverFamily,
364 phase: &mut PairingPhase,
365 commands: &mut mpsc::UnboundedReceiver<PairingCommand>,
366 notifications: &mut mpsc::UnboundedReceiver<HidppMessage>,
367 events: &mpsc::UnboundedSender<PairingEvent>,
368) -> Result<(), PairingError> {
369 write_register(channel, NOTIFICATIONS, NOTIFICATION_FLAGS).await?;
370
371 match family {
372 ReceiverFamily::Bolt => {
373 write_register(channel, BOLT_DISCOVERY, [DISCOVERY_TIMEOUT, 0x01, 0x00]).await?;
374 }
375 ReceiverFamily::Unifying => {
376 write_register(channel, UNIFYING_PAIRING, [0x01, 0x00, DISCOVERY_TIMEOUT]).await?;
377 }
378 }
379 let _ = events.send(PairingEvent::Searching);
380
381 let mut partial: HashMap<u16, PartialDevice> = HashMap::new();
383 let mut pairing_auth: Option<u8> = None;
385 let deadline = tokio::time::sleep(SESSION_TIMEOUT);
386 tokio::pin!(deadline);
387
388 loop {
389 tokio::select! {
390 () = &mut deadline => return Err(PairingError::Timeout),
391
392 cmd = commands.recv() => match cmd {
393 Some(PairingCommand::Pair(device)) => {
394 pairing_auth = Some(device.authentication);
395 if *phase == PairingPhase::BoltDiscovery {
396 *phase = PairingPhase::BoltPairing;
397 }
398 pair_bolt_device(channel, &device).await?;
399 }
400 Some(PairingCommand::Cancel) | None => {
401 return Err(PairingError::Cancelled);
402 }
403 },
404
405 msg = notifications.recv() => {
406 let Some(msg) = msg else {
407 return Err(PairingError::Hid("receiver channel closed".into()));
408 };
409 let (device_index, sub_id, payload) = decode(&msg);
410 trace!(sub_id = format_args!("{sub_id:#04x}"), ?payload, "pairing notification");
413 let Some(note) = parse_notification(sub_id, device_index, payload) else {
414 continue;
415 };
416 match note {
417 Notification::DiscoveryInfo { counter, kind, address, authentication } => {
418 let entry = partial.entry(counter).or_default();
419 entry.kind = Some(kind);
420 entry.address = Some(address);
421 entry.authentication = Some(authentication);
422 if let Some(device) = entry.build() {
423 let _ = events.send(PairingEvent::DeviceFound(device));
424 }
425 }
426 Notification::DiscoveryName { counter, name } => {
427 let entry = partial.entry(counter).or_default();
428 entry.name = Some(name);
429 if let Some(device) = entry.build() {
430 let _ = events.send(PairingEvent::DeviceFound(device));
431 }
432 }
433 Notification::Passkey { digits, value } => {
434 let method = match pairing_auth {
435 Some(auth) if auth & 0x01 != 0 => PasskeyMethod::Keyboard(digits),
436 _ => PasskeyMethod::Pointer {
437 clicks: passkey_to_clicks(value),
438 passkey: digits,
439 },
440 };
441 let _ = events.send(PairingEvent::Passkey(method));
442 }
443 Notification::MalformedPasskey => {
444 return Err(PairingError::MalformedNotification("passkey digits"));
445 }
446 Notification::PairingSucceeded { slot } => {
447 let _ = events.send(PairingEvent::Paired { slot });
448 return Ok(());
449 }
450 Notification::PairingError(code) => return Err(PairingError::Device(code)),
451 Notification::Connected { slot, established } if family == ReceiverFamily::Unifying => {
452 if established {
453 let _ = events.send(PairingEvent::Paired { slot });
454 return Ok(());
455 }
456 }
457 Notification::Connected { .. } => {}
458 Notification::UnifyingLock { open, error } => {
459 if error != 0 {
460 return Err(PairingError::Device(error));
461 }
462 if !open {
463 return Err(PairingError::Timeout);
465 }
466 }
467 }
468 }
469 }
470 }
471}
472
473#[derive(Default)]
475struct PartialDevice {
476 kind: Option<u8>,
477 address: Option<[u8; 6]>,
478 authentication: Option<u8>,
479 name: Option<String>,
480 emitted: bool,
481}
482
483impl PartialDevice {
484 fn build(&mut self) -> Option<DiscoveredDevice> {
486 if self.emitted {
487 return None;
488 }
489 let (kind, address, authentication, name) = (
490 self.kind?,
491 self.address?,
492 self.authentication?,
493 self.name.clone()?,
494 );
495 self.emitted = true;
496 Some(DiscoveredDevice {
497 address,
498 authentication,
499 kind: BoltDeviceKind::from(kind & 0x0f),
500 name,
501 })
502 }
503}
504
505async fn pair_bolt_device(
507 channel: &HidppChannel,
508 device: &DiscoveredDevice,
509) -> Result<(), PairingError> {
510 let mut payload = [0u8; 16];
511 payload[0] = 0x01; payload[1] = 0x00; payload[2..8].copy_from_slice(&device.address);
514 payload[8] = device.authentication;
515 payload[9] = device.entropy();
516 write_long_register(channel, BOLT_PAIRING, payload).await
517}
518
519async fn cancel(channel: &HidppChannel, phase: PairingPhase) {
521 let res = match phase {
522 PairingPhase::BoltDiscovery => {
523 write_register(channel, BOLT_DISCOVERY, [DISCOVERY_TIMEOUT, 0x02, 0x00]).await
524 }
525 PairingPhase::BoltPairing => {
526 let mut payload = [0u8; 16];
527 payload[0] = 0x02;
528 write_long_register(channel, BOLT_PAIRING, payload).await
529 }
530 PairingPhase::UnifyingPairing => {
531 write_register(channel, UNIFYING_PAIRING, [0x02, 0x00, 0x00]).await
532 }
533 };
534 if let Err(e) = res {
535 debug!(?phase, ?e, "cancel write failed");
536 }
537}
538
539pub async fn unpair(target: ReceiverSelector, slot: u8) -> Result<(), PairingError> {
541 let (channel, family) = open_receiver(&target).await?;
542 match family {
543 ReceiverFamily::Bolt => {
544 let mut payload = [0u8; 16];
545 payload[0] = 0x03; payload[1] = slot;
547 write_long_register(&channel, BOLT_PAIRING, payload).await
548 }
549 ReceiverFamily::Unifying => {
550 write_register(&channel, UNIFYING_PAIRING, [0x03, slot, 0x00]).await
551 }
552 }
553}
554
555#[cfg(test)]
556mod tests;