use std::{collections::HashMap, sync::Arc};
use hidpp::{
channel::{HidppChannel, HidppMessage},
receiver::{self, Receiver},
};
use tokio::sync::mpsc;
use tracing::{debug, trace};
pub use hidpp::receiver::bolt::DeviceKind as BoltDeviceKind;
pub use openlogi_core::hid::pairing::{Click, PairingError, PasskeyMethod, ReceiverSelector};
use openlogi_core::hid::DeviceRoute;
use crate::backend::{HidBackend, NodeId};
use crate::host_lock::{RECEIVER_REGISTER_TIMEOUT, ReceiverRegisterPhase, lock_receiver_registers};
mod notification;
mod registers;
use notification::{Notification, decode, parse_notification, subscribe};
use registers::{
BOLT_DISCOVERY, BOLT_PAIRING, NOTIFICATION_FLAGS, NOTIFICATIONS, UNIFYING_PAIRING,
write_long_register, write_register,
};
const RECEIVER_INDEX: u8 = 0xff;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ReceiverFamily {
Bolt,
Unifying,
}
enum SessionState {
BoltDiscovery(BoltDiscovery),
BoltPairing(BoltPairing),
UnifyingPairing,
}
impl From<ReceiverFamily> for SessionState {
fn from(family: ReceiverFamily) -> Self {
match family {
ReceiverFamily::Bolt => Self::BoltDiscovery(BoltDiscovery::default()),
ReceiverFamily::Unifying => Self::UnifyingPairing,
}
}
}
impl SessionState {
async fn open(&self, channel: &HidppChannel) -> Result<(), PairingError> {
match self {
Self::BoltDiscovery(_) => {
write_register(channel, BOLT_DISCOVERY, [DISCOVERY_TIMEOUT, 0x01, 0x00]).await
}
Self::UnifyingPairing => {
write_register(channel, UNIFYING_PAIRING, [0x01, 0x00, DISCOVERY_TIMEOUT]).await
}
Self::BoltPairing(_) => unreachable!("a session starts in receiver-open state"),
}
}
fn select_bolt_device(&mut self, device: &DiscoveredDevice) -> Result<(), PairingError> {
match self {
Self::BoltDiscovery(_) | Self::BoltPairing(_) => {
*self = Self::BoltPairing(BoltPairing {
authentication: device.authentication,
});
Ok(())
}
Self::UnifyingPairing => Err(PairingError::UnsupportedCommand),
}
}
}
#[derive(Default)]
struct BoltDiscovery {
partial: HashMap<u16, PartialDevice>,
}
struct BoltPairing {
authentication: u8,
}
fn family_for(product_id: u16) -> Option<ReceiverFamily> {
match crate::find_receiver(crate::LOGITECH_VENDOR_ID, product_id)?.protocol {
crate::ReceiverProtocol::Bolt => Some(ReceiverFamily::Bolt),
crate::ReceiverProtocol::Unifying => Some(ReceiverFamily::Unifying),
}
}
#[derive(Clone, Debug)]
pub struct PairingReceiver {
pub uid: Option<String>,
pub family: ReceiverFamily,
pub product_id: u16,
}
#[derive(Clone, Debug)]
pub struct DiscoveredDevice {
pub address: [u8; 6],
pub authentication: u8,
pub kind: BoltDeviceKind,
pub name: String,
}
impl DiscoveredDevice {
#[must_use]
pub fn passkey_on_keyboard(&self) -> bool {
self.authentication & 0x01 != 0
}
fn entropy(&self) -> u8 {
if self.kind == BoltDeviceKind::Keyboard {
20
} else {
10
}
}
}
fn passkey_to_clicks(value: u32) -> Vec<Click> {
(0..10)
.rev()
.map(|bit| {
if value & (1 << bit) != 0 {
Click::Right
} else {
Click::Left
}
})
.collect()
}
#[derive(Clone, Debug)]
pub enum PairingEvent {
Searching,
DeviceFound(DiscoveredDevice),
Passkey(PasskeyMethod),
Paired {
slot: u8,
},
Failed(PairingError),
}
#[derive(Clone, Debug)]
pub enum PairingCommand {
Pair(DiscoveredDevice),
Cancel,
}
pub async fn list_pairing_receivers(
backend: &dyn HidBackend,
) -> Result<Vec<PairingReceiver>, PairingError> {
let mut out = Vec::new();
for node in backend.enumerate_hidpp().await? {
let Some(channel) = backend.open_hidpp(&node).await? else {
continue;
};
let Some(family) = family_for(channel.product_id) else {
continue;
};
let uid = match family {
ReceiverFamily::Bolt => read_receiver_uid(&channel, &node.id).await,
ReceiverFamily::Unifying => None,
};
out.push(PairingReceiver {
uid,
family,
product_id: channel.product_id,
});
}
Ok(out)
}
async fn read_receiver_uid(channel: &Arc<HidppChannel>, node: &NodeId) -> Option<String> {
let receiver = receiver::detect(Arc::clone(channel))?;
let _registers = lock_receiver_registers(node, RECEIVER_REGISTER_TIMEOUT).await?;
match receiver {
Receiver::Bolt(bolt) => bolt.get_unique_id().await.ok(),
Receiver::Unifying(unifying) => unifying.get_unique_id().await.ok(),
_ => None,
}
}
enum ReceiverTarget<'a> {
Selector(&'a ReceiverSelector),
Route {
family: ReceiverFamily,
uid: &'a str,
},
}
struct OpenReceiver {
channel: Arc<HidppChannel>,
family: ReceiverFamily,
_registers: ReceiverRegisterPhase,
}
async fn open_receiver(
backend: &dyn HidBackend,
target: &ReceiverTarget<'_>,
) -> Result<OpenReceiver, PairingError> {
for node in backend.enumerate_hidpp().await? {
let Some(channel) = backend.open_hidpp(&node).await? else {
continue;
};
let Some(family) = family_for(channel.product_id) else {
continue;
};
let (want_family, want_uid) = match target {
ReceiverTarget::Selector(ReceiverSelector::First) => (None, None),
ReceiverTarget::Selector(ReceiverSelector::BoltUid(uid)) => {
(Some(ReceiverFamily::Bolt), Some(uid.as_str()))
}
ReceiverTarget::Route { family, uid } => (Some(*family), Some(*uid)),
};
if want_family.is_some_and(|want| want != family) {
continue;
}
let matched = match want_uid {
None => true,
Some(want) => read_receiver_uid(&channel, &node.id)
.await
.is_some_and(|uid| uid.eq_ignore_ascii_case(want)),
};
if !matched {
continue;
}
let Some(registers) = lock_receiver_registers(&node.id, RECEIVER_REGISTER_TIMEOUT).await
else {
return Err(PairingError::Register(
"the receiver's registers are held by another OpenLogi process".to_string(),
));
};
return Ok(OpenReceiver {
channel,
family,
_registers: registers,
});
}
Err(PairingError::ReceiverNotFound)
}
const SESSION_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(90);
const DISCOVERY_TIMEOUT: u8 = 30;
pub async fn run_pairing(
backend: &dyn HidBackend,
target: ReceiverSelector,
mut commands: mpsc::UnboundedReceiver<PairingCommand>,
events: mpsc::UnboundedSender<PairingEvent>,
) -> Result<(), PairingError> {
let receiver = match open_receiver(backend, &ReceiverTarget::Selector(&target)).await {
Ok(receiver) => receiver,
Err(e) => {
let _ = events.send(PairingEvent::Failed(e.clone()));
return Err(e);
}
};
let OpenReceiver {
channel, family, ..
} = &receiver;
let (listener, mut notifications) = subscribe(channel);
let result = run_session(channel, *family, &mut commands, &mut notifications, &events).await;
drop(listener);
let _ = channel
.write_register(RECEIVER_INDEX, NOTIFICATIONS, [0, 0, 0])
.await;
drop(receiver);
if let Err(ref e) = result {
let _ = events.send(PairingEvent::Failed(e.clone()));
}
result
}
async fn run_session(
channel: &HidppChannel,
family: ReceiverFamily,
commands: &mut mpsc::UnboundedReceiver<PairingCommand>,
notifications: &mut mpsc::UnboundedReceiver<HidppMessage>,
events: &mpsc::UnboundedSender<PairingEvent>,
) -> Result<(), PairingError> {
let mut state = SessionState::from(family);
let result = drive(channel, &mut state, commands, notifications, events).await;
if result.is_err() {
cancel(channel, &state).await;
}
result
}
async fn drive(
channel: &HidppChannel,
state: &mut SessionState,
commands: &mut mpsc::UnboundedReceiver<PairingCommand>,
notifications: &mut mpsc::UnboundedReceiver<HidppMessage>,
events: &mpsc::UnboundedSender<PairingEvent>,
) -> Result<(), PairingError> {
write_register(channel, NOTIFICATIONS, NOTIFICATION_FLAGS).await?;
state.open(channel).await?;
let _ = events.send(PairingEvent::Searching);
let deadline = tokio::time::sleep(SESSION_TIMEOUT);
tokio::pin!(deadline);
loop {
tokio::select! {
() = &mut deadline => return Err(PairingError::Timeout),
cmd = commands.recv() => match cmd {
Some(PairingCommand::Pair(device)) => {
state.select_bolt_device(&device)?;
pair_bolt_device(channel, &device).await?;
}
Some(PairingCommand::Cancel) | None => {
return Err(PairingError::Cancelled);
}
},
msg = notifications.recv() => {
let Some(msg) = msg else {
return Err(PairingError::Hid("receiver channel closed".into()));
};
let (device_index, sub_id, payload) = decode(&msg);
trace!(sub_id = format_args!("{sub_id:#04x}"), ?payload, "pairing notification");
let Some(note) = parse_notification(sub_id, device_index, payload) else {
continue;
};
match note {
Notification::DiscoveryInfo { counter, kind, address, authentication } => {
let SessionState::BoltDiscovery(discovery) = state else {
continue;
};
let entry = discovery.partial.entry(counter).or_default();
entry.kind = Some(kind);
entry.address = Some(address);
entry.authentication = Some(authentication);
if let Some(device) = entry.build() {
let _ = events.send(PairingEvent::DeviceFound(device));
}
}
Notification::DiscoveryName { counter, name } => {
let SessionState::BoltDiscovery(discovery) = state else {
continue;
};
let entry = discovery.partial.entry(counter).or_default();
entry.name = Some(name);
if let Some(device) = entry.build() {
let _ = events.send(PairingEvent::DeviceFound(device));
}
}
Notification::Passkey { digits, value } => {
let method = match state {
SessionState::BoltPairing(pairing)
if pairing.authentication & 0x01 != 0 =>
{
PasskeyMethod::Keyboard(digits)
}
_ => PasskeyMethod::Pointer {
clicks: passkey_to_clicks(value),
passkey: digits,
},
};
let _ = events.send(PairingEvent::Passkey(method));
}
Notification::MalformedPasskey => {
return Err(PairingError::MalformedNotification("passkey digits"));
}
Notification::PairingSucceeded { slot } => {
let _ = events.send(PairingEvent::Paired { slot });
return Ok(());
}
Notification::PairingError(code) => return Err(PairingError::Device(code)),
Notification::Connected { slot, established }
if matches!(state, SessionState::UnifyingPairing) => {
if established {
let _ = events.send(PairingEvent::Paired { slot });
return Ok(());
}
}
Notification::Connected { .. } => {}
Notification::UnifyingLock { open, error } => {
if error != 0 {
return Err(PairingError::Device(error));
}
if !open {
return Err(PairingError::Timeout);
}
}
}
}
}
}
}
#[derive(Default)]
struct PartialDevice {
kind: Option<u8>,
address: Option<[u8; 6]>,
authentication: Option<u8>,
name: Option<String>,
emitted: bool,
}
impl PartialDevice {
fn build(&mut self) -> Option<DiscoveredDevice> {
if self.emitted {
return None;
}
let (kind, address, authentication, name) = (
self.kind?,
self.address?,
self.authentication?,
self.name.clone()?,
);
self.emitted = true;
Some(DiscoveredDevice {
address,
authentication,
kind: BoltDeviceKind::from(kind & 0x0f),
name,
})
}
}
async fn pair_bolt_device(
channel: &HidppChannel,
device: &DiscoveredDevice,
) -> Result<(), PairingError> {
let mut payload = [0u8; 16];
payload[0] = 0x01; payload[1] = 0x00; payload[2..8].copy_from_slice(&device.address);
payload[8] = device.authentication;
payload[9] = device.entropy();
write_long_register(channel, BOLT_PAIRING, payload).await
}
async fn cancel(channel: &HidppChannel, state: &SessionState) {
let res = match state {
SessionState::BoltDiscovery(_) => {
write_register(channel, BOLT_DISCOVERY, [DISCOVERY_TIMEOUT, 0x02, 0x00]).await
}
SessionState::BoltPairing(_) => {
let mut payload = [0u8; 16];
payload[0] = 0x02;
write_long_register(channel, BOLT_PAIRING, payload).await
}
SessionState::UnifyingPairing => {
write_register(channel, UNIFYING_PAIRING, [0x02, 0x00, 0x00]).await
}
};
if let Err(e) = res {
let phase = match state {
SessionState::BoltDiscovery(_) => "Bolt discovery",
SessionState::BoltPairing(_) => "Bolt pairing",
SessionState::UnifyingPairing => "Unifying pairing",
};
debug!(phase, ?e, "cancel write failed");
}
}
pub async fn unpair(backend: &dyn HidBackend, route: &DeviceRoute) -> Result<(), PairingError> {
let (family, uid, slot) = match route {
DeviceRoute::Bolt { receiver_uid, slot } => (ReceiverFamily::Bolt, receiver_uid, *slot),
DeviceRoute::Unifying { receiver_uid, slot } => {
(ReceiverFamily::Unifying, receiver_uid, *slot)
}
DeviceRoute::Direct { .. } | DeviceRoute::RawHid { .. } => {
return Err(PairingError::ReceiverNotFound);
}
};
let receiver = open_receiver(backend, &ReceiverTarget::Route { family, uid }).await?;
let channel = &receiver.channel;
match receiver.family {
ReceiverFamily::Bolt => {
let mut payload = [0u8; 16];
payload[0] = 0x03; payload[1] = slot;
write_long_register(channel, BOLT_PAIRING, payload).await
}
ReceiverFamily::Unifying => {
write_register(channel, UNIFYING_PAIRING, [0x03, slot, 0x00]).await
}
}
}
#[cfg(test)]
mod tests;