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 crate::backend::{HidBackend, NodeId};
42use crate::host_lock::{RECEIVER_REGISTER_TIMEOUT, ReceiverRegisterPhase, lock_receiver_registers};
43
44mod notification;
45mod registers;
46
47use notification::{Notification, decode, parse_notification, subscribe};
48use registers::{
49 BOLT_DISCOVERY, BOLT_PAIRING, NOTIFICATION_FLAGS, NOTIFICATIONS, UNIFYING_PAIRING,
50 write_long_register, write_register,
51};
52
53const RECEIVER_INDEX: u8 = 0xff;
55
56#[derive(Clone, Copy, PartialEq, Eq, Debug)]
58pub enum ReceiverFamily {
59 Bolt,
61 Unifying,
63}
64
65enum SessionState {
66 BoltDiscovery(BoltDiscovery),
67 BoltPairing(BoltPairing),
68 UnifyingPairing,
69}
70
71impl From<ReceiverFamily> for SessionState {
72 fn from(family: ReceiverFamily) -> Self {
73 match family {
74 ReceiverFamily::Bolt => Self::BoltDiscovery(BoltDiscovery::default()),
75 ReceiverFamily::Unifying => Self::UnifyingPairing,
76 }
77 }
78}
79
80impl SessionState {
81 async fn open(&self, channel: &HidppChannel) -> Result<(), PairingError> {
82 match self {
83 Self::BoltDiscovery(_) => {
84 write_register(channel, BOLT_DISCOVERY, [DISCOVERY_TIMEOUT, 0x01, 0x00]).await
85 }
86 Self::UnifyingPairing => {
87 write_register(channel, UNIFYING_PAIRING, [0x01, 0x00, DISCOVERY_TIMEOUT]).await
88 }
89 Self::BoltPairing(_) => unreachable!("a session starts in receiver-open state"),
90 }
91 }
92
93 fn select_bolt_device(&mut self, device: &DiscoveredDevice) -> Result<(), PairingError> {
94 match self {
95 Self::BoltDiscovery(_) | Self::BoltPairing(_) => {
96 *self = Self::BoltPairing(BoltPairing {
97 authentication: device.authentication,
98 });
99 Ok(())
100 }
101 Self::UnifyingPairing => Err(PairingError::UnsupportedCommand),
102 }
103 }
104}
105
106#[derive(Default)]
107struct BoltDiscovery {
108 partial: HashMap<u16, PartialDevice>,
109}
110
111struct BoltPairing {
112 authentication: u8,
113}
114
115fn family_for(product_id: u16) -> Option<ReceiverFamily> {
116 match crate::find_receiver(crate::LOGITECH_VENDOR_ID, product_id)?.protocol {
117 crate::ReceiverProtocol::Bolt => Some(ReceiverFamily::Bolt),
118 crate::ReceiverProtocol::Unifying => Some(ReceiverFamily::Unifying),
119 }
120}
121
122#[derive(Clone, Debug)]
124pub struct PairingReceiver {
125 pub uid: Option<String>,
127 pub family: ReceiverFamily,
129 pub product_id: u16,
131}
132
133#[derive(Clone, Debug)]
135pub struct DiscoveredDevice {
136 pub address: [u8; 6],
138 pub authentication: u8,
140 pub kind: BoltDeviceKind,
142 pub name: String,
144}
145
146impl DiscoveredDevice {
147 #[must_use]
150 pub fn passkey_on_keyboard(&self) -> bool {
151 self.authentication & 0x01 != 0
152 }
153
154 fn entropy(&self) -> u8 {
156 if self.kind == BoltDeviceKind::Keyboard {
157 20
158 } else {
159 10
160 }
161 }
162}
163
164fn passkey_to_clicks(value: u32) -> Vec<Click> {
166 (0..10)
167 .rev()
168 .map(|bit| {
169 if value & (1 << bit) != 0 {
170 Click::Right
171 } else {
172 Click::Left
173 }
174 })
175 .collect()
176}
177
178#[derive(Clone, Debug)]
180pub enum PairingEvent {
181 Searching,
183 DeviceFound(DiscoveredDevice),
185 Passkey(PasskeyMethod),
187 Paired {
189 slot: u8,
191 },
192 Failed(PairingError),
194}
195
196#[derive(Clone, Debug)]
198pub enum PairingCommand {
199 Pair(DiscoveredDevice),
201 Cancel,
203}
204
205pub async fn list_pairing_receivers(
207 backend: &dyn HidBackend,
208) -> Result<Vec<PairingReceiver>, PairingError> {
209 let mut out = Vec::new();
210 for node in backend.enumerate_hidpp().await? {
211 let Some(channel) = backend.open_hidpp(&node).await? else {
212 continue;
213 };
214 let Some(family) = family_for(channel.product_id) else {
215 continue;
216 };
217 let uid = match family {
218 ReceiverFamily::Bolt => read_bolt_uid(&channel, &node.id).await,
219 ReceiverFamily::Unifying => None,
220 };
221 out.push(PairingReceiver {
222 uid,
223 family,
224 product_id: channel.product_id,
225 });
226 }
227 Ok(out)
228}
229
230async fn read_bolt_uid(channel: &Arc<HidppChannel>, node: &NodeId) -> Option<String> {
234 let Some(Receiver::Bolt(bolt)) = receiver::detect(Arc::clone(channel)) else {
235 return None;
236 };
237 let _registers = lock_receiver_registers(node, RECEIVER_REGISTER_TIMEOUT).await?;
238 bolt.get_unique_id().await.ok()
239}
240
241struct OpenReceiver {
243 channel: Arc<HidppChannel>,
244 family: ReceiverFamily,
245 _registers: ReceiverRegisterPhase,
252}
253
254async fn open_receiver(
257 backend: &dyn HidBackend,
258 target: &ReceiverSelector,
259) -> Result<OpenReceiver, PairingError> {
260 for node in backend.enumerate_hidpp().await? {
261 let Some(channel) = backend.open_hidpp(&node).await? else {
262 continue;
263 };
264 let Some(family) = family_for(channel.product_id) else {
265 continue;
266 };
267 let matched = match target {
268 ReceiverSelector::First => true,
269 ReceiverSelector::BoltUid(want) => {
270 family == ReceiverFamily::Bolt
271 && read_bolt_uid(&channel, &node.id)
272 .await
273 .is_some_and(|uid| uid.eq_ignore_ascii_case(want))
274 }
275 };
276 if !matched {
277 continue;
278 }
279 let Some(registers) = lock_receiver_registers(&node.id, RECEIVER_REGISTER_TIMEOUT).await
280 else {
281 return Err(PairingError::Register(
282 "the receiver's registers are held by another OpenLogi process".to_string(),
283 ));
284 };
285 return Ok(OpenReceiver {
286 channel,
287 family,
288 _registers: registers,
289 });
290 }
291 Err(PairingError::ReceiverNotFound)
292}
293
294const SESSION_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(90);
296const DISCOVERY_TIMEOUT: u8 = 30;
298
299pub async fn run_pairing(
307 backend: &dyn HidBackend,
308 target: ReceiverSelector,
309 mut commands: mpsc::UnboundedReceiver<PairingCommand>,
310 events: mpsc::UnboundedSender<PairingEvent>,
311) -> Result<(), PairingError> {
312 let receiver = match open_receiver(backend, &target).await {
313 Ok(receiver) => receiver,
314 Err(e) => {
315 let _ = events.send(PairingEvent::Failed(e.clone()));
316 return Err(e);
317 }
318 };
319 let OpenReceiver {
320 channel, family, ..
321 } = &receiver;
322 let (listener, mut notifications) = subscribe(channel);
323
324 let result = run_session(channel, *family, &mut commands, &mut notifications, &events).await;
325
326 drop(listener);
327 let _ = channel
329 .write_register(RECEIVER_INDEX, NOTIFICATIONS, [0, 0, 0])
330 .await;
331 drop(receiver);
333
334 if let Err(ref e) = result {
335 let _ = events.send(PairingEvent::Failed(e.clone()));
336 }
337 result
338}
339
340async fn run_session(
342 channel: &HidppChannel,
343 family: ReceiverFamily,
344 commands: &mut mpsc::UnboundedReceiver<PairingCommand>,
345 notifications: &mut mpsc::UnboundedReceiver<HidppMessage>,
346 events: &mpsc::UnboundedSender<PairingEvent>,
347) -> Result<(), PairingError> {
348 let mut state = SessionState::from(family);
349 let result = drive(channel, &mut state, commands, notifications, events).await;
350 if result.is_err() {
351 cancel(channel, &state).await;
352 }
353 result
354}
355
356async fn drive(
358 channel: &HidppChannel,
359 state: &mut SessionState,
360 commands: &mut mpsc::UnboundedReceiver<PairingCommand>,
361 notifications: &mut mpsc::UnboundedReceiver<HidppMessage>,
362 events: &mpsc::UnboundedSender<PairingEvent>,
363) -> Result<(), PairingError> {
364 write_register(channel, NOTIFICATIONS, NOTIFICATION_FLAGS).await?;
365 state.open(channel).await?;
366 let _ = events.send(PairingEvent::Searching);
367
368 let deadline = tokio::time::sleep(SESSION_TIMEOUT);
369 tokio::pin!(deadline);
370
371 loop {
372 tokio::select! {
373 () = &mut deadline => return Err(PairingError::Timeout),
374
375 cmd = commands.recv() => match cmd {
376 Some(PairingCommand::Pair(device)) => {
377 state.select_bolt_device(&device)?;
378 pair_bolt_device(channel, &device).await?;
379 }
380 Some(PairingCommand::Cancel) | None => {
381 return Err(PairingError::Cancelled);
382 }
383 },
384
385 msg = notifications.recv() => {
386 let Some(msg) = msg else {
387 return Err(PairingError::Hid("receiver channel closed".into()));
388 };
389 let (device_index, sub_id, payload) = decode(&msg);
390 trace!(sub_id = format_args!("{sub_id:#04x}"), ?payload, "pairing notification");
393 let Some(note) = parse_notification(sub_id, device_index, payload) else {
394 continue;
395 };
396 match note {
399 Notification::DiscoveryInfo { counter, kind, address, authentication } => {
400 let SessionState::BoltDiscovery(discovery) = state else {
401 continue;
402 };
403 let entry = discovery.partial.entry(counter).or_default();
404 entry.kind = Some(kind);
405 entry.address = Some(address);
406 entry.authentication = Some(authentication);
407 if let Some(device) = entry.build() {
408 let _ = events.send(PairingEvent::DeviceFound(device));
409 }
410 }
411 Notification::DiscoveryName { counter, name } => {
412 let SessionState::BoltDiscovery(discovery) = state else {
413 continue;
414 };
415 let entry = discovery.partial.entry(counter).or_default();
416 entry.name = Some(name);
417 if let Some(device) = entry.build() {
418 let _ = events.send(PairingEvent::DeviceFound(device));
419 }
420 }
421 Notification::Passkey { digits, value } => {
422 let method = match state {
423 SessionState::BoltPairing(pairing)
424 if pairing.authentication & 0x01 != 0 =>
425 {
426 PasskeyMethod::Keyboard(digits)
427 }
428 _ => PasskeyMethod::Pointer {
429 clicks: passkey_to_clicks(value),
430 passkey: digits,
431 },
432 };
433 let _ = events.send(PairingEvent::Passkey(method));
434 }
435 Notification::MalformedPasskey => {
436 return Err(PairingError::MalformedNotification("passkey digits"));
437 }
438 Notification::PairingSucceeded { slot } => {
439 let _ = events.send(PairingEvent::Paired { slot });
440 return Ok(());
441 }
442 Notification::PairingError(code) => return Err(PairingError::Device(code)),
443 Notification::Connected { slot, established }
444 if matches!(state, SessionState::UnifyingPairing) => {
445 if established {
446 let _ = events.send(PairingEvent::Paired { slot });
447 return Ok(());
448 }
449 }
450 Notification::Connected { .. } => {}
451 Notification::UnifyingLock { open, error } => {
452 if error != 0 {
453 return Err(PairingError::Device(error));
454 }
455 if !open {
456 return Err(PairingError::Timeout);
458 }
459 }
460 }
461 }
462 }
463 }
464}
465
466#[derive(Default)]
468struct PartialDevice {
469 kind: Option<u8>,
470 address: Option<[u8; 6]>,
471 authentication: Option<u8>,
472 name: Option<String>,
473 emitted: bool,
474}
475
476impl PartialDevice {
477 fn build(&mut self) -> Option<DiscoveredDevice> {
479 if self.emitted {
480 return None;
481 }
482 let (kind, address, authentication, name) = (
483 self.kind?,
484 self.address?,
485 self.authentication?,
486 self.name.clone()?,
487 );
488 self.emitted = true;
489 Some(DiscoveredDevice {
490 address,
491 authentication,
492 kind: BoltDeviceKind::from(kind & 0x0f),
493 name,
494 })
495 }
496}
497
498async fn pair_bolt_device(
500 channel: &HidppChannel,
501 device: &DiscoveredDevice,
502) -> Result<(), PairingError> {
503 let mut payload = [0u8; 16];
504 payload[0] = 0x01; payload[1] = 0x00; payload[2..8].copy_from_slice(&device.address);
507 payload[8] = device.authentication;
508 payload[9] = device.entropy();
509 write_long_register(channel, BOLT_PAIRING, payload).await
510}
511
512async fn cancel(channel: &HidppChannel, state: &SessionState) {
514 let res = match state {
515 SessionState::BoltDiscovery(_) => {
516 write_register(channel, BOLT_DISCOVERY, [DISCOVERY_TIMEOUT, 0x02, 0x00]).await
517 }
518 SessionState::BoltPairing(_) => {
519 let mut payload = [0u8; 16];
520 payload[0] = 0x02;
521 write_long_register(channel, BOLT_PAIRING, payload).await
522 }
523 SessionState::UnifyingPairing => {
524 write_register(channel, UNIFYING_PAIRING, [0x02, 0x00, 0x00]).await
525 }
526 };
527 if let Err(e) = res {
528 let phase = match state {
529 SessionState::BoltDiscovery(_) => "Bolt discovery",
530 SessionState::BoltPairing(_) => "Bolt pairing",
531 SessionState::UnifyingPairing => "Unifying pairing",
532 };
533 debug!(phase, ?e, "cancel write failed");
534 }
535}
536
537pub async fn unpair(
539 backend: &dyn HidBackend,
540 target: ReceiverSelector,
541 slot: u8,
542) -> Result<(), PairingError> {
543 let receiver = open_receiver(backend, &target).await?;
544 let channel = &receiver.channel;
545 match receiver.family {
546 ReceiverFamily::Bolt => {
547 let mut payload = [0u8; 16];
548 payload[0] = 0x03; payload[1] = slot;
550 write_long_register(channel, BOLT_PAIRING, payload).await
551 }
552 ReceiverFamily::Unifying => {
553 write_register(channel, UNIFYING_PAIRING, [0x03, slot, 0x00]).await
554 }
555 }
556}
557
558#[cfg(test)]
559mod tests;