1use std::{collections::HashMap, sync::Arc};
27
28use hidpp::{
29 channel::{HidppChannel, HidppMessage},
30 receiver::{self, 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, NOTIFICATION_FLAGS, NOTIFICATIONS, UNIFYING_PAIRING,
52 write_long_register, 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 UnifyingPairing,
71}
72
73impl From<ReceiverFamily> for SessionState {
74 fn from(family: ReceiverFamily) -> Self {
75 match family {
76 ReceiverFamily::Bolt => Self::BoltDiscovery(BoltDiscovery::default()),
77 ReceiverFamily::Unifying => Self::UnifyingPairing,
78 }
79 }
80}
81
82impl SessionState {
83 async fn open(&self, channel: &HidppChannel) -> Result<(), PairingError> {
84 match self {
85 Self::BoltDiscovery(_) => {
86 write_register(channel, BOLT_DISCOVERY, [DISCOVERY_TIMEOUT, 0x01, 0x00]).await
87 }
88 Self::UnifyingPairing => {
89 write_register(channel, UNIFYING_PAIRING, [0x01, 0x00, DISCOVERY_TIMEOUT]).await
90 }
91 Self::BoltPairing(_) => unreachable!("a session starts in receiver-open state"),
92 }
93 }
94
95 fn select_bolt_device(&mut self, device: &DiscoveredDevice) -> Result<(), PairingError> {
96 match self {
97 Self::BoltDiscovery(_) | Self::BoltPairing(_) => {
98 *self = Self::BoltPairing(BoltPairing {
99 authentication: device.authentication,
100 });
101 Ok(())
102 }
103 Self::UnifyingPairing => Err(PairingError::UnsupportedCommand),
104 }
105 }
106}
107
108#[derive(Default)]
109struct BoltDiscovery {
110 partial: HashMap<u16, PartialDevice>,
111}
112
113struct BoltPairing {
114 authentication: u8,
115}
116
117fn family_for(product_id: u16) -> Option<ReceiverFamily> {
118 match crate::find_receiver(crate::LOGITECH_VENDOR_ID, product_id)?.protocol {
119 crate::ReceiverProtocol::Bolt => Some(ReceiverFamily::Bolt),
120 crate::ReceiverProtocol::Unifying => Some(ReceiverFamily::Unifying),
121 }
122}
123
124#[derive(Clone, Debug)]
126pub struct PairingReceiver {
127 pub uid: Option<String>,
129 pub family: ReceiverFamily,
131 pub product_id: u16,
133}
134
135#[derive(Clone, Debug)]
137pub struct DiscoveredDevice {
138 pub address: [u8; 6],
140 pub authentication: u8,
142 pub kind: BoltDeviceKind,
144 pub name: String,
146}
147
148impl DiscoveredDevice {
149 #[must_use]
152 pub fn passkey_on_keyboard(&self) -> bool {
153 self.authentication & 0x01 != 0
154 }
155
156 fn entropy(&self) -> u8 {
158 if self.kind == BoltDeviceKind::Keyboard {
159 20
160 } else {
161 10
162 }
163 }
164}
165
166fn passkey_to_clicks(value: u32) -> Vec<Click> {
168 (0..10)
169 .rev()
170 .map(|bit| {
171 if value & (1 << bit) != 0 {
172 Click::Right
173 } else {
174 Click::Left
175 }
176 })
177 .collect()
178}
179
180#[derive(Clone, Debug)]
182pub enum PairingEvent {
183 Searching,
185 DeviceFound(DiscoveredDevice),
187 Passkey(PasskeyMethod),
189 Paired {
191 slot: u8,
193 },
194 Failed(PairingError),
196}
197
198#[derive(Clone, Debug)]
200pub enum PairingCommand {
201 Pair(DiscoveredDevice),
203 Cancel,
205}
206
207pub async fn list_pairing_receivers(
209 backend: &dyn HidBackend,
210) -> Result<Vec<PairingReceiver>, PairingError> {
211 let mut out = Vec::new();
212 for node in backend.enumerate_hidpp().await? {
213 let Some(channel) = backend.open_hidpp(&node).await? else {
214 continue;
215 };
216 let Some(family) = family_for(channel.product_id) else {
217 continue;
218 };
219 let uid = match family {
220 ReceiverFamily::Bolt => read_receiver_uid(&channel, &node.id).await,
221 ReceiverFamily::Unifying => None,
222 };
223 out.push(PairingReceiver {
224 uid,
225 family,
226 product_id: channel.product_id,
227 });
228 }
229 Ok(out)
230}
231
232async fn read_receiver_uid(channel: &Arc<HidppChannel>, node: &NodeId) -> Option<String> {
236 let receiver = receiver::detect(Arc::clone(channel))?;
237 let _registers = lock_receiver_registers(node, RECEIVER_REGISTER_TIMEOUT).await?;
238 match receiver {
239 Receiver::Bolt(bolt) => bolt.get_unique_id().await.ok(),
240 Receiver::Unifying(unifying) => unifying.get_unique_id().await.ok(),
241 _ => None,
244 }
245}
246
247enum ReceiverTarget<'a> {
249 Selector(&'a ReceiverSelector),
251 Route {
253 family: ReceiverFamily,
254 uid: &'a str,
255 },
256}
257
258struct OpenReceiver {
260 channel: Arc<HidppChannel>,
261 family: ReceiverFamily,
262 _registers: ReceiverRegisterPhase,
269}
270
271async fn open_receiver(
274 backend: &dyn HidBackend,
275 target: &ReceiverTarget<'_>,
276) -> Result<OpenReceiver, PairingError> {
277 for node in backend.enumerate_hidpp().await? {
278 let Some(channel) = backend.open_hidpp(&node).await? else {
279 continue;
280 };
281 let Some(family) = family_for(channel.product_id) else {
282 continue;
283 };
284 let (want_family, want_uid) = match target {
285 ReceiverTarget::Selector(ReceiverSelector::First) => (None, None),
286 ReceiverTarget::Selector(ReceiverSelector::BoltUid(uid)) => {
287 (Some(ReceiverFamily::Bolt), Some(uid.as_str()))
288 }
289 ReceiverTarget::Route { family, uid } => (Some(*family), Some(*uid)),
290 };
291 if want_family.is_some_and(|want| want != family) {
292 continue;
293 }
294 let matched = match want_uid {
295 None => true,
296 Some(want) => read_receiver_uid(&channel, &node.id)
297 .await
298 .is_some_and(|uid| uid.eq_ignore_ascii_case(want)),
299 };
300 if !matched {
301 continue;
302 }
303 let Some(registers) = lock_receiver_registers(&node.id, RECEIVER_REGISTER_TIMEOUT).await
304 else {
305 return Err(PairingError::Register(
306 "the receiver's registers are held by another OpenLogi process".to_string(),
307 ));
308 };
309 return Ok(OpenReceiver {
310 channel,
311 family,
312 _registers: registers,
313 });
314 }
315 Err(PairingError::ReceiverNotFound)
316}
317
318const SESSION_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(90);
320const DISCOVERY_TIMEOUT: u8 = 30;
322
323pub async fn run_pairing(
331 backend: &dyn HidBackend,
332 target: ReceiverSelector,
333 mut commands: mpsc::UnboundedReceiver<PairingCommand>,
334 events: mpsc::UnboundedSender<PairingEvent>,
335) -> Result<(), PairingError> {
336 let receiver = match open_receiver(backend, &ReceiverTarget::Selector(&target)).await {
337 Ok(receiver) => receiver,
338 Err(e) => {
339 let _ = events.send(PairingEvent::Failed(e.clone()));
340 return Err(e);
341 }
342 };
343 let OpenReceiver {
344 channel, family, ..
345 } = &receiver;
346 let (listener, mut notifications) = subscribe(channel);
347
348 let result = run_session(channel, *family, &mut commands, &mut notifications, &events).await;
349
350 drop(listener);
351 let _ = channel
353 .write_register(RECEIVER_INDEX, NOTIFICATIONS, [0, 0, 0])
354 .await;
355 drop(receiver);
357
358 if let Err(ref e) = result {
359 let _ = events.send(PairingEvent::Failed(e.clone()));
360 }
361 result
362}
363
364async fn run_session(
366 channel: &HidppChannel,
367 family: ReceiverFamily,
368 commands: &mut mpsc::UnboundedReceiver<PairingCommand>,
369 notifications: &mut mpsc::UnboundedReceiver<HidppMessage>,
370 events: &mpsc::UnboundedSender<PairingEvent>,
371) -> Result<(), PairingError> {
372 let mut state = SessionState::from(family);
373 let result = drive(channel, &mut state, commands, notifications, events).await;
374 if result.is_err() {
375 cancel(channel, &state).await;
376 }
377 result
378}
379
380async fn drive(
382 channel: &HidppChannel,
383 state: &mut SessionState,
384 commands: &mut mpsc::UnboundedReceiver<PairingCommand>,
385 notifications: &mut mpsc::UnboundedReceiver<HidppMessage>,
386 events: &mpsc::UnboundedSender<PairingEvent>,
387) -> Result<(), PairingError> {
388 write_register(channel, NOTIFICATIONS, NOTIFICATION_FLAGS).await?;
389 state.open(channel).await?;
390 let _ = events.send(PairingEvent::Searching);
391
392 let deadline = tokio::time::sleep(SESSION_TIMEOUT);
393 tokio::pin!(deadline);
394
395 loop {
396 tokio::select! {
397 () = &mut deadline => return Err(PairingError::Timeout),
398
399 cmd = commands.recv() => match cmd {
400 Some(PairingCommand::Pair(device)) => {
401 state.select_bolt_device(&device)?;
402 pair_bolt_device(channel, &device).await?;
403 }
404 Some(PairingCommand::Cancel) | None => {
405 return Err(PairingError::Cancelled);
406 }
407 },
408
409 msg = notifications.recv() => {
410 let Some(msg) = msg else {
411 return Err(PairingError::Hid("receiver channel closed".into()));
412 };
413 let (device_index, sub_id, payload) = decode(&msg);
414 trace!(sub_id = format_args!("{sub_id:#04x}"), ?payload, "pairing notification");
417 let Some(note) = parse_notification(sub_id, device_index, payload) else {
418 continue;
419 };
420 match note {
423 Notification::DiscoveryInfo { counter, kind, address, authentication } => {
424 let SessionState::BoltDiscovery(discovery) = state else {
425 continue;
426 };
427 let entry = discovery.partial.entry(counter).or_default();
428 entry.kind = Some(kind);
429 entry.address = Some(address);
430 entry.authentication = Some(authentication);
431 if let Some(device) = entry.build() {
432 let _ = events.send(PairingEvent::DeviceFound(device));
433 }
434 }
435 Notification::DiscoveryName { counter, name } => {
436 let SessionState::BoltDiscovery(discovery) = state else {
437 continue;
438 };
439 let entry = discovery.partial.entry(counter).or_default();
440 entry.name = Some(name);
441 if let Some(device) = entry.build() {
442 let _ = events.send(PairingEvent::DeviceFound(device));
443 }
444 }
445 Notification::Passkey { digits, value } => {
446 let method = match state {
447 SessionState::BoltPairing(pairing)
448 if pairing.authentication & 0x01 != 0 =>
449 {
450 PasskeyMethod::Keyboard(digits)
451 }
452 _ => PasskeyMethod::Pointer {
453 clicks: passkey_to_clicks(value),
454 passkey: digits,
455 },
456 };
457 let _ = events.send(PairingEvent::Passkey(method));
458 }
459 Notification::MalformedPasskey => {
460 return Err(PairingError::MalformedNotification("passkey digits"));
461 }
462 Notification::PairingSucceeded { slot } => {
463 let _ = events.send(PairingEvent::Paired { slot });
464 return Ok(());
465 }
466 Notification::PairingError(code) => return Err(PairingError::Device(code)),
467 Notification::Connected { slot, established }
468 if matches!(state, SessionState::UnifyingPairing) => {
469 if established {
470 let _ = events.send(PairingEvent::Paired { slot });
471 return Ok(());
472 }
473 }
474 Notification::Connected { .. } => {}
475 Notification::UnifyingLock { open, error } => {
476 if error != 0 {
477 return Err(PairingError::Device(error));
478 }
479 if !open {
480 return Err(PairingError::Timeout);
482 }
483 }
484 }
485 }
486 }
487 }
488}
489
490#[derive(Default)]
492struct PartialDevice {
493 kind: Option<u8>,
494 address: Option<[u8; 6]>,
495 authentication: Option<u8>,
496 name: Option<String>,
497 emitted: bool,
498}
499
500impl PartialDevice {
501 fn build(&mut self) -> Option<DiscoveredDevice> {
503 if self.emitted {
504 return None;
505 }
506 let (kind, address, authentication, name) = (
507 self.kind?,
508 self.address?,
509 self.authentication?,
510 self.name.clone()?,
511 );
512 self.emitted = true;
513 Some(DiscoveredDevice {
514 address,
515 authentication,
516 kind: BoltDeviceKind::from(kind & 0x0f),
517 name,
518 })
519 }
520}
521
522async fn pair_bolt_device(
524 channel: &HidppChannel,
525 device: &DiscoveredDevice,
526) -> Result<(), PairingError> {
527 let mut payload = [0u8; 16];
528 payload[0] = 0x01; payload[1] = 0x00; payload[2..8].copy_from_slice(&device.address);
531 payload[8] = device.authentication;
532 payload[9] = device.entropy();
533 write_long_register(channel, BOLT_PAIRING, payload).await
534}
535
536async fn cancel(channel: &HidppChannel, state: &SessionState) {
538 let res = match state {
539 SessionState::BoltDiscovery(_) => {
540 write_register(channel, BOLT_DISCOVERY, [DISCOVERY_TIMEOUT, 0x02, 0x00]).await
541 }
542 SessionState::BoltPairing(_) => {
543 let mut payload = [0u8; 16];
544 payload[0] = 0x02;
545 write_long_register(channel, BOLT_PAIRING, payload).await
546 }
547 SessionState::UnifyingPairing => {
548 write_register(channel, UNIFYING_PAIRING, [0x02, 0x00, 0x00]).await
549 }
550 };
551 if let Err(e) = res {
552 let phase = match state {
553 SessionState::BoltDiscovery(_) => "Bolt discovery",
554 SessionState::BoltPairing(_) => "Bolt pairing",
555 SessionState::UnifyingPairing => "Unifying pairing",
556 };
557 debug!(phase, ?e, "cancel write failed");
558 }
559}
560
561pub async fn unpair(backend: &dyn HidBackend, route: &DeviceRoute) -> Result<(), PairingError> {
566 let (family, uid, slot) = match route {
567 DeviceRoute::Bolt { receiver_uid, slot } => (ReceiverFamily::Bolt, receiver_uid, *slot),
568 DeviceRoute::Unifying { receiver_uid, slot } => {
569 (ReceiverFamily::Unifying, receiver_uid, *slot)
570 }
571 DeviceRoute::Direct { .. } | DeviceRoute::RawHid { .. } => {
572 return Err(PairingError::ReceiverNotFound);
573 }
574 };
575 let receiver = open_receiver(backend, &ReceiverTarget::Route { family, uid }).await?;
576 let channel = &receiver.channel;
577 match receiver.family {
578 ReceiverFamily::Bolt => {
579 let mut payload = [0u8; 16];
580 payload[0] = 0x03; payload[1] = slot;
582 write_long_register(channel, BOLT_PAIRING, payload).await
583 }
584 ReceiverFamily::Unifying => {
585 write_register(channel, UNIFYING_PAIRING, [0x03, slot, 0x00]).await
586 }
587 }
588}
589
590#[cfg(test)]
591mod tests;