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