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
64#[derive(Clone, Copy, PartialEq, Eq, Debug)]
65enum PairingPhase {
66 BoltDiscovery,
67 BoltPairing,
68 UnifyingPairing,
69}
70
71impl From<ReceiverFamily> for PairingPhase {
72 fn from(family: ReceiverFamily) -> Self {
73 match family {
74 ReceiverFamily::Bolt => Self::BoltDiscovery,
75 ReceiverFamily::Unifying => Self::UnifyingPairing,
76 }
77 }
78}
79
80fn family_for(product_id: u16) -> Option<ReceiverFamily> {
81 if crate::BOLT_PIDS.contains(&product_id) {
82 Some(ReceiverFamily::Bolt)
83 } else if crate::speaks_unifying_protocol(product_id) {
84 Some(ReceiverFamily::Unifying)
86 } else {
87 None
88 }
89}
90
91#[derive(Clone, Debug)]
93pub struct PairingReceiver {
94 pub uid: Option<String>,
96 pub family: ReceiverFamily,
98 pub product_id: u16,
100}
101
102#[derive(Clone, Debug)]
104pub struct DiscoveredDevice {
105 pub address: [u8; 6],
107 pub authentication: u8,
109 pub kind: BoltDeviceKind,
111 pub name: String,
113}
114
115impl DiscoveredDevice {
116 #[must_use]
119 pub fn passkey_on_keyboard(&self) -> bool {
120 self.authentication & 0x01 != 0
121 }
122
123 fn entropy(&self) -> u8 {
125 if self.kind == BoltDeviceKind::Keyboard {
126 20
127 } else {
128 10
129 }
130 }
131}
132
133fn passkey_to_clicks(value: u32) -> Vec<Click> {
135 (0..10)
136 .rev()
137 .map(|bit| {
138 if value & (1 << bit) != 0 {
139 Click::Right
140 } else {
141 Click::Left
142 }
143 })
144 .collect()
145}
146
147#[derive(Clone, Debug)]
149pub enum PairingEvent {
150 Searching,
152 DeviceFound(DiscoveredDevice),
154 Passkey(PasskeyMethod),
156 Paired {
158 slot: u8,
160 },
161 Failed(PairingError),
163}
164
165#[derive(Clone, Debug)]
167pub enum PairingCommand {
168 Pair(DiscoveredDevice),
170 Cancel,
172}
173
174pub async fn list_pairing_receivers(
176 backend: &dyn HidBackend,
177) -> Result<Vec<PairingReceiver>, PairingError> {
178 let mut out = Vec::new();
179 for node in backend.enumerate_hidpp().await? {
180 let Some(channel) = backend.open_hidpp(&node).await? else {
181 continue;
182 };
183 let Some(family) = family_for(channel.product_id) else {
184 continue;
185 };
186 let uid = match family {
187 ReceiverFamily::Bolt => read_bolt_uid(&channel).await,
188 ReceiverFamily::Unifying => None,
189 };
190 out.push(PairingReceiver {
191 uid,
192 family,
193 product_id: channel.product_id,
194 });
195 }
196 Ok(out)
197}
198
199async fn read_bolt_uid(channel: &Arc<HidppChannel>) -> Option<String> {
201 let Some(Receiver::Bolt(bolt)) = receiver::detect(Arc::clone(channel)) else {
202 return None;
203 };
204 bolt.get_unique_id().await.ok()
205}
206
207async fn open_receiver(
209 backend: &dyn HidBackend,
210 target: &ReceiverSelector,
211) -> Result<(Arc<HidppChannel>, ReceiverFamily), PairingError> {
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 match target {
220 ReceiverSelector::First => return Ok((channel, family)),
221 ReceiverSelector::BoltUid(want) => {
222 if family == ReceiverFamily::Bolt
223 && read_bolt_uid(&channel)
224 .await
225 .is_some_and(|uid| uid.eq_ignore_ascii_case(want))
226 {
227 return Ok((channel, family));
228 }
229 }
230 }
231 }
232 Err(PairingError::ReceiverNotFound)
233}
234
235const SESSION_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(90);
237const DISCOVERY_TIMEOUT: u8 = 30;
239
240pub async fn run_pairing(
248 backend: &dyn HidBackend,
249 target: ReceiverSelector,
250 mut commands: mpsc::UnboundedReceiver<PairingCommand>,
251 events: mpsc::UnboundedSender<PairingEvent>,
252) -> Result<(), PairingError> {
253 let (channel, family) = match open_receiver(backend, &target).await {
254 Ok(receiver) => receiver,
255 Err(e) => {
256 let _ = events.send(PairingEvent::Failed(e.clone()));
257 return Err(e);
258 }
259 };
260 let (listener, mut notifications) = subscribe(&channel);
261
262 let result = run_session(&channel, family, &mut commands, &mut notifications, &events).await;
263
264 drop(listener);
265 let _ = channel
267 .write_register(RECEIVER_INDEX, NOTIFICATIONS, [0, 0, 0])
268 .await;
269
270 if let Err(ref e) = result {
271 let _ = events.send(PairingEvent::Failed(e.clone()));
272 }
273 result
274}
275
276async fn run_session(
278 channel: &HidppChannel,
279 family: ReceiverFamily,
280 commands: &mut mpsc::UnboundedReceiver<PairingCommand>,
281 notifications: &mut mpsc::UnboundedReceiver<HidppMessage>,
282 events: &mpsc::UnboundedSender<PairingEvent>,
283) -> Result<(), PairingError> {
284 let mut phase = PairingPhase::from(family);
285 let result = drive(channel, family, &mut phase, commands, notifications, events).await;
286 if result.is_err() {
287 cancel(channel, phase).await;
288 }
289 result
290}
291
292async fn drive(
294 channel: &HidppChannel,
295 family: ReceiverFamily,
296 phase: &mut PairingPhase,
297 commands: &mut mpsc::UnboundedReceiver<PairingCommand>,
298 notifications: &mut mpsc::UnboundedReceiver<HidppMessage>,
299 events: &mpsc::UnboundedSender<PairingEvent>,
300) -> Result<(), PairingError> {
301 write_register(channel, NOTIFICATIONS, NOTIFICATION_FLAGS).await?;
302
303 match family {
304 ReceiverFamily::Bolt => {
305 write_register(channel, BOLT_DISCOVERY, [DISCOVERY_TIMEOUT, 0x01, 0x00]).await?;
306 }
307 ReceiverFamily::Unifying => {
308 write_register(channel, UNIFYING_PAIRING, [0x01, 0x00, DISCOVERY_TIMEOUT]).await?;
309 }
310 }
311 let _ = events.send(PairingEvent::Searching);
312
313 let mut partial: HashMap<u16, PartialDevice> = HashMap::new();
315 let mut pairing_auth: Option<u8> = None;
317 let deadline = tokio::time::sleep(SESSION_TIMEOUT);
318 tokio::pin!(deadline);
319
320 loop {
321 tokio::select! {
322 () = &mut deadline => return Err(PairingError::Timeout),
323
324 cmd = commands.recv() => match cmd {
325 Some(PairingCommand::Pair(device)) => {
326 pairing_auth = Some(device.authentication);
327 if *phase == PairingPhase::BoltDiscovery {
328 *phase = PairingPhase::BoltPairing;
329 }
330 pair_bolt_device(channel, &device).await?;
331 }
332 Some(PairingCommand::Cancel) | None => {
333 return Err(PairingError::Cancelled);
334 }
335 },
336
337 msg = notifications.recv() => {
338 let Some(msg) = msg else {
339 return Err(PairingError::Hid("receiver channel closed".into()));
340 };
341 let (device_index, sub_id, payload) = decode(&msg);
342 trace!(sub_id = format_args!("{sub_id:#04x}"), ?payload, "pairing notification");
345 let Some(note) = parse_notification(sub_id, device_index, payload) else {
346 continue;
347 };
348 match note {
349 Notification::DiscoveryInfo { counter, kind, address, authentication } => {
350 let entry = partial.entry(counter).or_default();
351 entry.kind = Some(kind);
352 entry.address = Some(address);
353 entry.authentication = Some(authentication);
354 if let Some(device) = entry.build() {
355 let _ = events.send(PairingEvent::DeviceFound(device));
356 }
357 }
358 Notification::DiscoveryName { counter, name } => {
359 let entry = partial.entry(counter).or_default();
360 entry.name = Some(name);
361 if let Some(device) = entry.build() {
362 let _ = events.send(PairingEvent::DeviceFound(device));
363 }
364 }
365 Notification::Passkey { digits, value } => {
366 let method = match pairing_auth {
367 Some(auth) if auth & 0x01 != 0 => PasskeyMethod::Keyboard(digits),
368 _ => PasskeyMethod::Pointer {
369 clicks: passkey_to_clicks(value),
370 passkey: digits,
371 },
372 };
373 let _ = events.send(PairingEvent::Passkey(method));
374 }
375 Notification::MalformedPasskey => {
376 return Err(PairingError::MalformedNotification("passkey digits"));
377 }
378 Notification::PairingSucceeded { slot } => {
379 let _ = events.send(PairingEvent::Paired { slot });
380 return Ok(());
381 }
382 Notification::PairingError(code) => return Err(PairingError::Device(code)),
383 Notification::Connected { slot, established } if family == ReceiverFamily::Unifying => {
384 if established {
385 let _ = events.send(PairingEvent::Paired { slot });
386 return Ok(());
387 }
388 }
389 Notification::Connected { .. } => {}
390 Notification::UnifyingLock { open, error } => {
391 if error != 0 {
392 return Err(PairingError::Device(error));
393 }
394 if !open {
395 return Err(PairingError::Timeout);
397 }
398 }
399 }
400 }
401 }
402 }
403}
404
405#[derive(Default)]
407struct PartialDevice {
408 kind: Option<u8>,
409 address: Option<[u8; 6]>,
410 authentication: Option<u8>,
411 name: Option<String>,
412 emitted: bool,
413}
414
415impl PartialDevice {
416 fn build(&mut self) -> Option<DiscoveredDevice> {
418 if self.emitted {
419 return None;
420 }
421 let (kind, address, authentication, name) = (
422 self.kind?,
423 self.address?,
424 self.authentication?,
425 self.name.clone()?,
426 );
427 self.emitted = true;
428 Some(DiscoveredDevice {
429 address,
430 authentication,
431 kind: BoltDeviceKind::from(kind & 0x0f),
432 name,
433 })
434 }
435}
436
437async fn pair_bolt_device(
439 channel: &HidppChannel,
440 device: &DiscoveredDevice,
441) -> Result<(), PairingError> {
442 let mut payload = [0u8; 16];
443 payload[0] = 0x01; payload[1] = 0x00; payload[2..8].copy_from_slice(&device.address);
446 payload[8] = device.authentication;
447 payload[9] = device.entropy();
448 write_long_register(channel, BOLT_PAIRING, payload).await
449}
450
451async fn cancel(channel: &HidppChannel, phase: PairingPhase) {
453 let res = match phase {
454 PairingPhase::BoltDiscovery => {
455 write_register(channel, BOLT_DISCOVERY, [DISCOVERY_TIMEOUT, 0x02, 0x00]).await
456 }
457 PairingPhase::BoltPairing => {
458 let mut payload = [0u8; 16];
459 payload[0] = 0x02;
460 write_long_register(channel, BOLT_PAIRING, payload).await
461 }
462 PairingPhase::UnifyingPairing => {
463 write_register(channel, UNIFYING_PAIRING, [0x02, 0x00, 0x00]).await
464 }
465 };
466 if let Err(e) = res {
467 debug!(?phase, ?e, "cancel write failed");
468 }
469}
470
471pub async fn unpair(
473 backend: &dyn HidBackend,
474 target: ReceiverSelector,
475 slot: u8,
476) -> Result<(), PairingError> {
477 let (channel, family) = open_receiver(backend, &target).await?;
478 match family {
479 ReceiverFamily::Bolt => {
480 let mut payload = [0u8; 16];
481 payload[0] = 0x03; payload[1] = slot;
483 write_long_register(&channel, BOLT_PAIRING, payload).await
484 }
485 ReceiverFamily::Unifying => {
486 write_register(&channel, UNIFYING_PAIRING, [0x03, slot, 0x00]).await
487 }
488 }
489}
490
491#[cfg(test)]
492mod tests;