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;
42
43mod notification;
44mod registers;
45
46use notification::{Notification, decode, parse_notification, subscribe};
47use registers::{
48 BOLT_DISCOVERY, BOLT_PAIRING, NOTIFICATION_FLAGS, NOTIFICATIONS, UNIFYING_PAIRING,
49 write_long_register, write_register,
50};
51
52const RECEIVER_INDEX: u8 = 0xff;
54
55#[derive(Clone, Copy, PartialEq, Eq, Debug)]
57pub enum ReceiverFamily {
58 Bolt,
60 Unifying,
62}
63
64enum SessionState {
65 BoltDiscovery(BoltDiscovery),
66 BoltPairing(BoltPairing),
67 UnifyingPairing,
68}
69
70impl From<ReceiverFamily> for SessionState {
71 fn from(family: ReceiverFamily) -> Self {
72 match family {
73 ReceiverFamily::Bolt => Self::BoltDiscovery(BoltDiscovery::default()),
74 ReceiverFamily::Unifying => Self::UnifyingPairing,
75 }
76 }
77}
78
79impl SessionState {
80 async fn open(&self, channel: &HidppChannel) -> Result<(), PairingError> {
81 match self {
82 Self::BoltDiscovery(_) => {
83 write_register(channel, BOLT_DISCOVERY, [DISCOVERY_TIMEOUT, 0x01, 0x00]).await
84 }
85 Self::UnifyingPairing => {
86 write_register(channel, UNIFYING_PAIRING, [0x01, 0x00, DISCOVERY_TIMEOUT]).await
87 }
88 Self::BoltPairing(_) => unreachable!("a session starts in receiver-open state"),
89 }
90 }
91
92 fn select_bolt_device(&mut self, device: &DiscoveredDevice) -> Result<(), PairingError> {
93 match self {
94 Self::BoltDiscovery(_) | Self::BoltPairing(_) => {
95 *self = Self::BoltPairing(BoltPairing {
96 authentication: device.authentication,
97 });
98 Ok(())
99 }
100 Self::UnifyingPairing => Err(PairingError::UnsupportedCommand),
101 }
102 }
103}
104
105#[derive(Default)]
106struct BoltDiscovery {
107 partial: HashMap<u16, PartialDevice>,
108}
109
110struct BoltPairing {
111 authentication: u8,
112}
113
114fn family_for(product_id: u16) -> Option<ReceiverFamily> {
115 match crate::find_receiver(crate::LOGITECH_VENDOR_ID, product_id)?.protocol {
116 crate::ReceiverProtocol::Bolt => Some(ReceiverFamily::Bolt),
117 crate::ReceiverProtocol::Unifying => Some(ReceiverFamily::Unifying),
118 }
119}
120
121#[derive(Clone, Debug)]
123pub struct PairingReceiver {
124 pub uid: Option<String>,
126 pub family: ReceiverFamily,
128 pub product_id: u16,
130}
131
132#[derive(Clone, Debug)]
134pub struct DiscoveredDevice {
135 pub address: [u8; 6],
137 pub authentication: u8,
139 pub kind: BoltDeviceKind,
141 pub name: String,
143}
144
145impl DiscoveredDevice {
146 #[must_use]
149 pub fn passkey_on_keyboard(&self) -> bool {
150 self.authentication & 0x01 != 0
151 }
152
153 fn entropy(&self) -> u8 {
155 if self.kind == BoltDeviceKind::Keyboard {
156 20
157 } else {
158 10
159 }
160 }
161}
162
163fn passkey_to_clicks(value: u32) -> Vec<Click> {
165 (0..10)
166 .rev()
167 .map(|bit| {
168 if value & (1 << bit) != 0 {
169 Click::Right
170 } else {
171 Click::Left
172 }
173 })
174 .collect()
175}
176
177#[derive(Clone, Debug)]
179pub enum PairingEvent {
180 Searching,
182 DeviceFound(DiscoveredDevice),
184 Passkey(PasskeyMethod),
186 Paired {
188 slot: u8,
190 },
191 Failed(PairingError),
193}
194
195#[derive(Clone, Debug)]
197pub enum PairingCommand {
198 Pair(DiscoveredDevice),
200 Cancel,
202}
203
204pub async fn list_pairing_receivers(
206 backend: &dyn HidBackend,
207) -> Result<Vec<PairingReceiver>, PairingError> {
208 let mut out = Vec::new();
209 for node in backend.enumerate_hidpp().await? {
210 let Some(channel) = backend.open_hidpp(&node).await? else {
211 continue;
212 };
213 let Some(family) = family_for(channel.product_id) else {
214 continue;
215 };
216 let uid = match family {
217 ReceiverFamily::Bolt => read_bolt_uid(&channel).await,
218 ReceiverFamily::Unifying => None,
219 };
220 out.push(PairingReceiver {
221 uid,
222 family,
223 product_id: channel.product_id,
224 });
225 }
226 Ok(out)
227}
228
229async fn read_bolt_uid(channel: &Arc<HidppChannel>) -> Option<String> {
231 let Some(Receiver::Bolt(bolt)) = receiver::detect(Arc::clone(channel)) else {
232 return None;
233 };
234 bolt.get_unique_id().await.ok()
235}
236
237async fn open_receiver(
239 backend: &dyn HidBackend,
240 target: &ReceiverSelector,
241) -> Result<(Arc<HidppChannel>, ReceiverFamily), PairingError> {
242 for node in backend.enumerate_hidpp().await? {
243 let Some(channel) = backend.open_hidpp(&node).await? else {
244 continue;
245 };
246 let Some(family) = family_for(channel.product_id) else {
247 continue;
248 };
249 match target {
250 ReceiverSelector::First => return Ok((channel, family)),
251 ReceiverSelector::BoltUid(want) => {
252 if family == ReceiverFamily::Bolt
253 && read_bolt_uid(&channel)
254 .await
255 .is_some_and(|uid| uid.eq_ignore_ascii_case(want))
256 {
257 return Ok((channel, family));
258 }
259 }
260 }
261 }
262 Err(PairingError::ReceiverNotFound)
263}
264
265const SESSION_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(90);
267const DISCOVERY_TIMEOUT: u8 = 30;
269
270pub async fn run_pairing(
278 backend: &dyn HidBackend,
279 target: ReceiverSelector,
280 mut commands: mpsc::UnboundedReceiver<PairingCommand>,
281 events: mpsc::UnboundedSender<PairingEvent>,
282) -> Result<(), PairingError> {
283 let (channel, family) = match open_receiver(backend, &target).await {
284 Ok(receiver) => receiver,
285 Err(e) => {
286 let _ = events.send(PairingEvent::Failed(e.clone()));
287 return Err(e);
288 }
289 };
290 let (listener, mut notifications) = subscribe(&channel);
291
292 let result = run_session(&channel, family, &mut commands, &mut notifications, &events).await;
293
294 drop(listener);
295 let _ = channel
297 .write_register(RECEIVER_INDEX, NOTIFICATIONS, [0, 0, 0])
298 .await;
299
300 if let Err(ref e) = result {
301 let _ = events.send(PairingEvent::Failed(e.clone()));
302 }
303 result
304}
305
306async fn run_session(
308 channel: &HidppChannel,
309 family: ReceiverFamily,
310 commands: &mut mpsc::UnboundedReceiver<PairingCommand>,
311 notifications: &mut mpsc::UnboundedReceiver<HidppMessage>,
312 events: &mpsc::UnboundedSender<PairingEvent>,
313) -> Result<(), PairingError> {
314 let mut state = SessionState::from(family);
315 let result = drive(channel, &mut state, commands, notifications, events).await;
316 if result.is_err() {
317 cancel(channel, &state).await;
318 }
319 result
320}
321
322async fn drive(
324 channel: &HidppChannel,
325 state: &mut SessionState,
326 commands: &mut mpsc::UnboundedReceiver<PairingCommand>,
327 notifications: &mut mpsc::UnboundedReceiver<HidppMessage>,
328 events: &mpsc::UnboundedSender<PairingEvent>,
329) -> Result<(), PairingError> {
330 write_register(channel, NOTIFICATIONS, NOTIFICATION_FLAGS).await?;
331 state.open(channel).await?;
332 let _ = events.send(PairingEvent::Searching);
333
334 let deadline = tokio::time::sleep(SESSION_TIMEOUT);
335 tokio::pin!(deadline);
336
337 loop {
338 tokio::select! {
339 () = &mut deadline => return Err(PairingError::Timeout),
340
341 cmd = commands.recv() => match cmd {
342 Some(PairingCommand::Pair(device)) => {
343 state.select_bolt_device(&device)?;
344 pair_bolt_device(channel, &device).await?;
345 }
346 Some(PairingCommand::Cancel) | None => {
347 return Err(PairingError::Cancelled);
348 }
349 },
350
351 msg = notifications.recv() => {
352 let Some(msg) = msg else {
353 return Err(PairingError::Hid("receiver channel closed".into()));
354 };
355 let (device_index, sub_id, payload) = decode(&msg);
356 trace!(sub_id = format_args!("{sub_id:#04x}"), ?payload, "pairing notification");
359 let Some(note) = parse_notification(sub_id, device_index, payload) else {
360 continue;
361 };
362 match note {
365 Notification::DiscoveryInfo { counter, kind, address, authentication } => {
366 let SessionState::BoltDiscovery(discovery) = state else {
367 continue;
368 };
369 let entry = discovery.partial.entry(counter).or_default();
370 entry.kind = Some(kind);
371 entry.address = Some(address);
372 entry.authentication = Some(authentication);
373 if let Some(device) = entry.build() {
374 let _ = events.send(PairingEvent::DeviceFound(device));
375 }
376 }
377 Notification::DiscoveryName { counter, name } => {
378 let SessionState::BoltDiscovery(discovery) = state else {
379 continue;
380 };
381 let entry = discovery.partial.entry(counter).or_default();
382 entry.name = Some(name);
383 if let Some(device) = entry.build() {
384 let _ = events.send(PairingEvent::DeviceFound(device));
385 }
386 }
387 Notification::Passkey { digits, value } => {
388 let method = match state {
389 SessionState::BoltPairing(pairing)
390 if pairing.authentication & 0x01 != 0 =>
391 {
392 PasskeyMethod::Keyboard(digits)
393 }
394 _ => PasskeyMethod::Pointer {
395 clicks: passkey_to_clicks(value),
396 passkey: digits,
397 },
398 };
399 let _ = events.send(PairingEvent::Passkey(method));
400 }
401 Notification::MalformedPasskey => {
402 return Err(PairingError::MalformedNotification("passkey digits"));
403 }
404 Notification::PairingSucceeded { slot } => {
405 let _ = events.send(PairingEvent::Paired { slot });
406 return Ok(());
407 }
408 Notification::PairingError(code) => return Err(PairingError::Device(code)),
409 Notification::Connected { slot, established }
410 if matches!(state, SessionState::UnifyingPairing) => {
411 if established {
412 let _ = events.send(PairingEvent::Paired { slot });
413 return Ok(());
414 }
415 }
416 Notification::Connected { .. } => {}
417 Notification::UnifyingLock { open, error } => {
418 if error != 0 {
419 return Err(PairingError::Device(error));
420 }
421 if !open {
422 return Err(PairingError::Timeout);
424 }
425 }
426 }
427 }
428 }
429 }
430}
431
432#[derive(Default)]
434struct PartialDevice {
435 kind: Option<u8>,
436 address: Option<[u8; 6]>,
437 authentication: Option<u8>,
438 name: Option<String>,
439 emitted: bool,
440}
441
442impl PartialDevice {
443 fn build(&mut self) -> Option<DiscoveredDevice> {
445 if self.emitted {
446 return None;
447 }
448 let (kind, address, authentication, name) = (
449 self.kind?,
450 self.address?,
451 self.authentication?,
452 self.name.clone()?,
453 );
454 self.emitted = true;
455 Some(DiscoveredDevice {
456 address,
457 authentication,
458 kind: BoltDeviceKind::from(kind & 0x0f),
459 name,
460 })
461 }
462}
463
464async fn pair_bolt_device(
466 channel: &HidppChannel,
467 device: &DiscoveredDevice,
468) -> Result<(), PairingError> {
469 let mut payload = [0u8; 16];
470 payload[0] = 0x01; payload[1] = 0x00; payload[2..8].copy_from_slice(&device.address);
473 payload[8] = device.authentication;
474 payload[9] = device.entropy();
475 write_long_register(channel, BOLT_PAIRING, payload).await
476}
477
478async fn cancel(channel: &HidppChannel, state: &SessionState) {
480 let res = match state {
481 SessionState::BoltDiscovery(_) => {
482 write_register(channel, BOLT_DISCOVERY, [DISCOVERY_TIMEOUT, 0x02, 0x00]).await
483 }
484 SessionState::BoltPairing(_) => {
485 let mut payload = [0u8; 16];
486 payload[0] = 0x02;
487 write_long_register(channel, BOLT_PAIRING, payload).await
488 }
489 SessionState::UnifyingPairing => {
490 write_register(channel, UNIFYING_PAIRING, [0x02, 0x00, 0x00]).await
491 }
492 };
493 if let Err(e) = res {
494 let phase = match state {
495 SessionState::BoltDiscovery(_) => "Bolt discovery",
496 SessionState::BoltPairing(_) => "Bolt pairing",
497 SessionState::UnifyingPairing => "Unifying pairing",
498 };
499 debug!(phase, ?e, "cancel write failed");
500 }
501}
502
503pub async fn unpair(
505 backend: &dyn HidBackend,
506 target: ReceiverSelector,
507 slot: u8,
508) -> Result<(), PairingError> {
509 let (channel, family) = open_receiver(backend, &target).await?;
510 match family {
511 ReceiverFamily::Bolt => {
512 let mut payload = [0u8; 16];
513 payload[0] = 0x03; payload[1] = slot;
515 write_long_register(&channel, BOLT_PAIRING, payload).await
516 }
517 ReceiverFamily::Unifying => {
518 write_register(&channel, UNIFYING_PAIRING, [0x03, slot, 0x00]).await
519 }
520 }
521}
522
523#[cfg(test)]
524mod tests;