1use std::{collections::HashMap, sync::Arc};
27
28use hidpp::{
29 channel::{HidppChannel, HidppMessage},
30 receiver::{self, NotificationFlags, Receiver},
31};
32use tokio::sync::mpsc;
33use tracing::{debug, trace};
34
35pub use hidpp::receiver::bolt::DeviceKind as BoltDeviceKind;
36pub use openlogi_core::hid::pairing::{Click, PairingError, PasskeyMethod, ReceiverSelector};
40
41use openlogi_core::hid::DeviceRoute;
42
43use crate::backend::{HidBackend, NodeId};
44use crate::host_lock::{RECEIVER_REGISTER_TIMEOUT, ReceiverRegisterPhase, lock_receiver_registers};
45
46mod notification;
47mod registers;
48
49use notification::{Notification, decode, parse_notification, subscribe};
50use registers::{
51 BOLT_DISCOVERY, BOLT_PAIRING, PAIRING_NOTIFICATIONS, UNIFYING_PAIRING, read_notification_flags,
52 write_long_register, write_notification_flags, write_register,
53};
54
55const RECEIVER_INDEX: u8 = 0xff;
57
58#[derive(Clone, Copy, PartialEq, Eq, Debug)]
60pub enum ReceiverFamily {
61 Bolt,
63 Unifying,
65}
66
67enum SessionState {
68 BoltDiscovery(BoltDiscovery),
69 BoltPairing(BoltPairing),
70 UnifyingOpening,
74 UnifyingPairing,
76}
77
78impl From<ReceiverFamily> for SessionState {
79 fn from(family: ReceiverFamily) -> Self {
80 match family {
81 ReceiverFamily::Bolt => Self::BoltDiscovery(BoltDiscovery::default()),
82 ReceiverFamily::Unifying => Self::UnifyingOpening,
83 }
84 }
85}
86
87impl SessionState {
88 async fn open(&self, channel: &HidppChannel) -> Result<(), PairingError> {
89 match self {
90 Self::BoltDiscovery(_) => {
91 write_register(channel, BOLT_DISCOVERY, [DISCOVERY_TIMEOUT, 0x01, 0x00]).await
92 }
93 Self::UnifyingOpening => {
94 write_register(channel, UNIFYING_PAIRING, [0x01, 0x00, DISCOVERY_TIMEOUT]).await
95 }
96 Self::BoltPairing(_) | Self::UnifyingPairing => {
97 unreachable!("a session starts in receiver-open state")
98 }
99 }
100 }
101
102 fn select_bolt_device(&mut self, device: &DiscoveredDevice) -> Result<(), PairingError> {
103 match self {
104 Self::BoltDiscovery(_) | Self::BoltPairing(_) => {
105 *self = Self::BoltPairing(BoltPairing {
106 authentication: device.authentication,
107 });
108 Ok(())
109 }
110 Self::UnifyingOpening | Self::UnifyingPairing => Err(PairingError::UnsupportedCommand),
111 }
112 }
113
114 fn unifying_lock(&mut self, open: bool, error: u8) -> Result<bool, PairingError> {
117 if error != 0 {
118 return Err(PairingError::Device(error));
119 }
120 match (open, &*self) {
121 (true, Self::UnifyingOpening) => {
122 *self = Self::UnifyingPairing;
123 Ok(true)
124 }
125 (true, _) => Ok(false),
126 (false, Self::UnifyingOpening) => Err(lock_not_opened()),
128 (false, _) => Err(PairingError::Timeout),
130 }
131 }
132}
133
134#[derive(Default)]
135struct BoltDiscovery {
136 partial: HashMap<u16, PartialDevice>,
137}
138
139struct BoltPairing {
140 authentication: u8,
141}
142
143fn family_for(product_id: u16) -> Option<ReceiverFamily> {
144 match crate::find_receiver(crate::LOGITECH_VENDOR_ID, product_id)?.protocol {
145 crate::ReceiverProtocol::Bolt => Some(ReceiverFamily::Bolt),
146 crate::ReceiverProtocol::Unifying => Some(ReceiverFamily::Unifying),
147 }
148}
149
150#[derive(Clone, Debug)]
152pub struct PairingReceiver {
153 pub uid: Option<String>,
155 pub family: ReceiverFamily,
157 pub product_id: u16,
159}
160
161#[derive(Clone, Debug)]
163pub struct DiscoveredDevice {
164 pub address: [u8; 6],
166 pub authentication: u8,
168 pub kind: BoltDeviceKind,
170 pub name: String,
172}
173
174impl DiscoveredDevice {
175 #[must_use]
178 pub fn passkey_on_keyboard(&self) -> bool {
179 self.authentication & 0x01 != 0
180 }
181
182 fn entropy(&self) -> u8 {
184 if self.kind == BoltDeviceKind::Keyboard {
185 20
186 } else {
187 10
188 }
189 }
190}
191
192fn passkey_to_clicks(value: u32) -> Vec<Click> {
194 (0..10)
195 .rev()
196 .map(|bit| {
197 if value & (1 << bit) != 0 {
198 Click::Right
199 } else {
200 Click::Left
201 }
202 })
203 .collect()
204}
205
206#[derive(Clone, Debug)]
208pub enum PairingEvent {
209 Searching,
211 DeviceFound(DiscoveredDevice),
213 Passkey(PasskeyMethod),
215 Paired {
217 slot: u8,
219 },
220 Failed(PairingError),
222}
223
224#[derive(Clone, Debug)]
226pub enum PairingCommand {
227 Pair(DiscoveredDevice),
229 Cancel,
231}
232
233pub async fn list_pairing_receivers(
235 backend: &dyn HidBackend,
236) -> Result<Vec<PairingReceiver>, PairingError> {
237 let mut out = Vec::new();
238 for node in backend.enumerate_hidpp().await? {
239 let Some(channel) = backend.open_hidpp(&node).await? else {
240 continue;
241 };
242 let Some(family) = family_for(channel.product_id) else {
243 continue;
244 };
245 let uid = match family {
246 ReceiverFamily::Bolt => read_receiver_uid(&channel, &node.id).await,
247 ReceiverFamily::Unifying => None,
248 };
249 out.push(PairingReceiver {
250 uid,
251 family,
252 product_id: channel.product_id,
253 });
254 }
255 Ok(out)
256}
257
258async fn read_receiver_uid(channel: &Arc<HidppChannel>, node: &NodeId) -> Option<String> {
262 let receiver = receiver::detect(Arc::clone(channel))?;
263 let _registers = lock_receiver_registers(node, RECEIVER_REGISTER_TIMEOUT).await?;
264 match receiver {
265 Receiver::Bolt(bolt) => bolt.get_unique_id().await.ok(),
266 Receiver::Unifying(unifying) => unifying.get_unique_id().await.ok(),
267 _ => None,
270 }
271}
272
273enum ReceiverTarget<'a> {
275 Selector(&'a ReceiverSelector),
277 Route {
279 family: ReceiverFamily,
280 uid: &'a str,
281 },
282}
283
284struct OpenReceiver {
286 channel: Arc<HidppChannel>,
287 family: ReceiverFamily,
288 _registers: ReceiverRegisterPhase,
295}
296
297async fn open_receiver(
300 backend: &dyn HidBackend,
301 target: &ReceiverTarget<'_>,
302) -> Result<OpenReceiver, PairingError> {
303 for node in backend.enumerate_hidpp().await? {
304 let Some(channel) = backend.open_hidpp(&node).await? else {
305 continue;
306 };
307 let Some(family) = family_for(channel.product_id) else {
308 continue;
309 };
310 let (want_family, want_uid) = match target {
311 ReceiverTarget::Selector(ReceiverSelector::First) => (None, None),
312 ReceiverTarget::Selector(ReceiverSelector::BoltUid(uid)) => {
313 (Some(ReceiverFamily::Bolt), Some(uid.as_str()))
314 }
315 ReceiverTarget::Route { family, uid } => (Some(*family), Some(*uid)),
316 };
317 if want_family.is_some_and(|want| want != family) {
318 continue;
319 }
320 let matched = match want_uid {
321 None => true,
322 Some(want) => read_receiver_uid(&channel, &node.id)
323 .await
324 .is_some_and(|uid| uid.eq_ignore_ascii_case(want)),
325 };
326 if !matched {
327 continue;
328 }
329 let Some(registers) = lock_receiver_registers(&node.id, RECEIVER_REGISTER_TIMEOUT).await
330 else {
331 return Err(PairingError::Register(
332 "the receiver's registers are held by another OpenLogi process".to_string(),
333 ));
334 };
335 return Ok(OpenReceiver {
336 channel,
337 family,
338 _registers: registers,
339 });
340 }
341 Err(PairingError::ReceiverNotFound)
342}
343
344const SESSION_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(90);
346const DISCOVERY_TIMEOUT: u8 = 30;
348const LOCK_CONFIRMATION_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);
353
354fn lock_not_opened() -> PairingError {
357 PairingError::Register("the receiver did not open the pairing lock".to_owned())
358}
359
360pub async fn run_pairing(
368 backend: &dyn HidBackend,
369 target: ReceiverSelector,
370 mut commands: mpsc::UnboundedReceiver<PairingCommand>,
371 events: mpsc::UnboundedSender<PairingEvent>,
372) -> Result<(), PairingError> {
373 let receiver = match open_receiver(backend, &ReceiverTarget::Selector(&target)).await {
374 Ok(receiver) => receiver,
375 Err(e) => {
376 let _ = events.send(PairingEvent::Failed(e.clone()));
377 return Err(e);
378 }
379 };
380 let OpenReceiver {
381 channel, family, ..
382 } = &receiver;
383 let (listener, mut notifications) = subscribe(channel);
384
385 let original = read_notification_flags(channel).await;
386 let result = match &original {
387 Ok(original) => {
388 run_session(
389 channel,
390 *family,
391 *original,
392 &mut commands,
393 &mut notifications,
394 &events,
395 )
396 .await
397 }
398 Err(e) => Err(e.clone()),
399 };
400
401 drop(listener);
402 if let Ok(original) = original {
406 let _ = write_notification_flags(channel, original).await;
407 }
408 drop(receiver);
410
411 if let Err(ref e) = result {
412 let _ = events.send(PairingEvent::Failed(e.clone()));
413 }
414 result
415}
416
417async fn run_session(
421 channel: &HidppChannel,
422 family: ReceiverFamily,
423 notifications_found: NotificationFlags,
424 commands: &mut mpsc::UnboundedReceiver<PairingCommand>,
425 notifications: &mut mpsc::UnboundedReceiver<HidppMessage>,
426 events: &mpsc::UnboundedSender<PairingEvent>,
427) -> Result<(), PairingError> {
428 let mut state = SessionState::from(family);
429 let result = drive(
430 channel,
431 &mut state,
432 notifications_found,
433 commands,
434 notifications,
435 events,
436 )
437 .await;
438 if result.is_err() {
439 cancel(channel, &state).await;
440 }
441 result
442}
443
444async fn drive(
446 channel: &HidppChannel,
447 state: &mut SessionState,
448 notifications_found: NotificationFlags,
449 commands: &mut mpsc::UnboundedReceiver<PairingCommand>,
450 notifications: &mut mpsc::UnboundedReceiver<HidppMessage>,
451 events: &mpsc::UnboundedSender<PairingEvent>,
452) -> Result<(), PairingError> {
453 write_notification_flags(channel, notifications_found | PAIRING_NOTIFICATIONS).await?;
454 state.open(channel).await?;
455 if !matches!(state, SessionState::UnifyingOpening) {
460 let _ = events.send(PairingEvent::Searching);
461 }
462
463 let deadline = tokio::time::sleep(SESSION_TIMEOUT);
464 tokio::pin!(deadline);
465 let lock_confirmation = tokio::time::sleep(LOCK_CONFIRMATION_TIMEOUT);
466 tokio::pin!(lock_confirmation);
467
468 loop {
469 let awaiting_lock = matches!(state, SessionState::UnifyingOpening);
470 tokio::select! {
471 () = &mut deadline => return Err(PairingError::Timeout),
472 () = &mut lock_confirmation, if awaiting_lock => return Err(lock_not_opened()),
473
474 cmd = commands.recv() => match cmd {
475 Some(PairingCommand::Pair(device)) => {
476 state.select_bolt_device(&device)?;
477 pair_bolt_device(channel, &device).await?;
478 }
479 Some(PairingCommand::Cancel) | None => {
480 return Err(PairingError::Cancelled);
481 }
482 },
483
484 msg = notifications.recv() => {
485 let Some(msg) = msg else {
486 return Err(PairingError::Hid("receiver channel closed".into()));
487 };
488 let (device_index, sub_id, payload) = decode(&msg);
489 trace!(sub_id = format_args!("{sub_id:#04x}"), ?payload, "pairing notification");
492 let Some(note) = parse_notification(sub_id, device_index, payload) else {
493 continue;
494 };
495 match note {
498 Notification::DiscoveryInfo { counter, kind, address, authentication } => {
499 let SessionState::BoltDiscovery(discovery) = state else {
500 continue;
501 };
502 let entry = discovery.partial.entry(counter).or_default();
503 entry.kind = Some(kind);
504 entry.address = Some(address);
505 entry.authentication = Some(authentication);
506 if let Some(device) = entry.build() {
507 let _ = events.send(PairingEvent::DeviceFound(device));
508 }
509 }
510 Notification::DiscoveryName { counter, name } => {
511 let SessionState::BoltDiscovery(discovery) = state else {
512 continue;
513 };
514 let entry = discovery.partial.entry(counter).or_default();
515 entry.name = Some(name);
516 if let Some(device) = entry.build() {
517 let _ = events.send(PairingEvent::DeviceFound(device));
518 }
519 }
520 Notification::Passkey { digits, value } => {
521 let method = match state {
522 SessionState::BoltPairing(pairing)
523 if pairing.authentication & 0x01 != 0 =>
524 {
525 PasskeyMethod::Keyboard(digits)
526 }
527 _ => PasskeyMethod::Pointer {
528 clicks: passkey_to_clicks(value),
529 passkey: digits,
530 },
531 };
532 let _ = events.send(PairingEvent::Passkey(method));
533 }
534 Notification::MalformedPasskey => {
535 return Err(PairingError::MalformedNotification("passkey digits"));
536 }
537 Notification::PairingSucceeded { slot } => {
538 let _ = events.send(PairingEvent::Paired { slot });
539 return Ok(());
540 }
541 Notification::PairingError(code) => return Err(PairingError::Device(code)),
542 Notification::Connected { slot, established }
543 if matches!(state, SessionState::UnifyingPairing) => {
544 if established {
545 let _ = events.send(PairingEvent::Paired { slot });
546 return Ok(());
547 }
548 }
549 Notification::Connected { .. } => {}
550 Notification::UnifyingLock { open, error } => {
551 if state.unifying_lock(open, error)? {
552 let _ = events.send(PairingEvent::Searching);
553 }
554 }
555 }
556 }
557 }
558 }
559}
560
561#[derive(Default)]
563struct PartialDevice {
564 kind: Option<u8>,
565 address: Option<[u8; 6]>,
566 authentication: Option<u8>,
567 name: Option<String>,
568 emitted: bool,
569}
570
571impl PartialDevice {
572 fn build(&mut self) -> Option<DiscoveredDevice> {
574 if self.emitted {
575 return None;
576 }
577 let (kind, address, authentication, name) = (
578 self.kind?,
579 self.address?,
580 self.authentication?,
581 self.name.clone()?,
582 );
583 self.emitted = true;
584 Some(DiscoveredDevice {
585 address,
586 authentication,
587 kind: BoltDeviceKind::from(kind & 0x0f),
588 name,
589 })
590 }
591}
592
593async fn pair_bolt_device(
595 channel: &HidppChannel,
596 device: &DiscoveredDevice,
597) -> Result<(), PairingError> {
598 let mut payload = [0u8; 16];
599 payload[0] = 0x01; payload[1] = 0x00; payload[2..8].copy_from_slice(&device.address);
602 payload[8] = device.authentication;
603 payload[9] = device.entropy();
604 write_long_register(channel, BOLT_PAIRING, payload).await
605}
606
607async fn cancel(channel: &HidppChannel, state: &SessionState) {
609 let res = match state {
610 SessionState::BoltDiscovery(_) => {
611 write_register(channel, BOLT_DISCOVERY, [DISCOVERY_TIMEOUT, 0x02, 0x00]).await
612 }
613 SessionState::BoltPairing(_) => {
614 let mut payload = [0u8; 16];
615 payload[0] = 0x02;
616 write_long_register(channel, BOLT_PAIRING, payload).await
617 }
618 SessionState::UnifyingOpening | SessionState::UnifyingPairing => {
621 write_register(channel, UNIFYING_PAIRING, [0x02, 0x00, 0x00]).await
622 }
623 };
624 if let Err(e) = res {
625 let phase = match state {
626 SessionState::BoltDiscovery(_) => "Bolt discovery",
627 SessionState::BoltPairing(_) => "Bolt pairing",
628 SessionState::UnifyingOpening | SessionState::UnifyingPairing => "Unifying pairing",
629 };
630 debug!(phase, ?e, "cancel write failed");
631 }
632}
633
634pub async fn unpair(backend: &dyn HidBackend, route: &DeviceRoute) -> Result<(), PairingError> {
639 let (family, uid, slot) = match route {
640 DeviceRoute::Bolt { receiver_uid, slot } => (ReceiverFamily::Bolt, receiver_uid, *slot),
641 DeviceRoute::Unifying { receiver_uid, slot } => {
642 (ReceiverFamily::Unifying, receiver_uid, *slot)
643 }
644 DeviceRoute::Direct { .. } | DeviceRoute::RawHid { .. } => {
645 return Err(PairingError::ReceiverNotFound);
646 }
647 };
648 let receiver = open_receiver(backend, &ReceiverTarget::Route { family, uid }).await?;
649 let channel = &receiver.channel;
650 match receiver.family {
651 ReceiverFamily::Bolt => {
652 let mut payload = [0u8; 16];
653 payload[0] = 0x03; payload[1] = slot;
655 write_long_register(channel, BOLT_PAIRING, payload).await
656 }
657 ReceiverFamily::Unifying => {
658 write_register(channel, UNIFYING_PAIRING, [0x03, slot, 0x00]).await
659 }
660 }
661}
662
663#[cfg(test)]
664mod tests;