use std::fmt;
use std::sync::Arc;
use hidpp::{
channel::HidppChannel,
receiver::{self, Receiver},
};
use openlogi_core::device::DeviceInventory;
use serde::{Deserialize, Serialize};
use crate::transport::{enumerate_hidpp_devices, open_hidpp_channel};
pub const DIRECT_DEVICE_INDEX: u8 = 0xff;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeviceRoute {
Bolt { receiver_uid: String, slot: u8 },
Unifying { receiver_uid: String, slot: u8 },
Direct { vendor_id: u16, product_id: u16 },
}
pub const BOLT_PIDS: &[u16] = &[0xc548];
pub const UNIFYING_PIDS: &[u16] = &[0xc52b, 0xc532];
impl DeviceRoute {
#[must_use]
pub fn device_index(&self) -> u8 {
match self {
Self::Bolt { slot, .. } | Self::Unifying { slot, .. } => *slot,
Self::Direct { .. } => DIRECT_DEVICE_INDEX,
}
}
#[must_use]
pub fn device_route_for(inv: &DeviceInventory, slot: u8) -> Option<Self> {
match &inv.receiver.unique_id {
Some(uid) if UNIFYING_PIDS.contains(&inv.receiver.product_id) => Some(Self::Unifying {
receiver_uid: uid.clone(),
slot,
}),
Some(uid) => {
if !BOLT_PIDS.contains(&inv.receiver.product_id) {
tracing::debug!(
pid = format_args!("{:04x}", inv.receiver.product_id),
"unknown receiver PID — routing as Bolt"
);
}
Some(Self::Bolt {
receiver_uid: uid.clone(),
slot,
})
}
None if slot == DIRECT_DEVICE_INDEX => Some(Self::Direct {
vendor_id: inv.receiver.vendor_id,
product_id: inv.receiver.product_id,
}),
None => None,
}
}
}
impl fmt::Display for DeviceRoute {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Bolt { receiver_uid, slot } | Self::Unifying { receiver_uid, slot } => {
write!(f, "slot {slot} on receiver {receiver_uid}")
}
Self::Direct {
vendor_id,
product_id,
} => write!(f, "direct {vendor_id:04x}:{product_id:04x}"),
}
}
}
pub(crate) async fn open_route_channel(
route: &DeviceRoute,
) -> Result<Option<Arc<HidppChannel>>, async_hid::HidError> {
let candidates = enumerate_hidpp_devices().await?;
for dev in candidates {
if let DeviceRoute::Direct {
vendor_id,
product_id,
} = route
&& (dev.vendor_id != *vendor_id || dev.product_id != *product_id)
{
continue;
}
let Some((_, channel)) = open_hidpp_channel(dev).await? else {
continue;
};
match route {
DeviceRoute::Bolt { receiver_uid, .. } => {
let Some(Receiver::Bolt(bolt)) = receiver::detect(Arc::clone(&channel)) else {
continue;
};
if let Ok(uid) = bolt.get_unique_id().await
&& uid.eq_ignore_ascii_case(receiver_uid)
{
return Ok(Some(channel));
}
}
DeviceRoute::Unifying { receiver_uid, .. } => {
let Some(Receiver::Unifying(unifying)) = receiver::detect(Arc::clone(&channel))
else {
continue;
};
if let Ok(uid) = unifying.get_unique_id().await
&& uid.eq_ignore_ascii_case(receiver_uid)
{
return Ok(Some(channel));
}
}
DeviceRoute::Direct { .. } => return Ok(Some(channel)),
}
}
Ok(None)
}
#[cfg(test)]
mod tests {
use openlogi_core::device::{DeviceInventory, ReceiverInfo};
use super::{DIRECT_DEVICE_INDEX, DeviceRoute, UNIFYING_PIDS};
fn inv(product_id: u16, unique_id: Option<&str>) -> DeviceInventory {
DeviceInventory {
receiver: ReceiverInfo {
name: "test".into(),
vendor_id: 0x046d,
product_id,
unique_id: unique_id.map(str::to_string),
},
paired: vec![],
}
}
#[test]
fn device_route_for_unifying_pids_create_unifying_route() {
for &pid in UNIFYING_PIDS {
let route = DeviceRoute::device_route_for(&inv(pid, Some("A1B2")), 2);
assert!(
matches!(route, Some(DeviceRoute::Unifying { ref receiver_uid, slot: 2 }) if receiver_uid == "A1B2"),
"pid {pid:#06x} should produce Unifying route"
);
}
}
#[test]
fn device_route_for_bolt_pid_creates_bolt_route() {
let route = DeviceRoute::device_route_for(&inv(0xc548, Some("UID")), 1);
assert!(matches!(
route,
Some(DeviceRoute::Bolt { ref receiver_uid, slot: 1 }) if receiver_uid == "UID"
));
}
#[test]
fn device_route_for_direct_when_no_uid_and_direct_slot() {
let route = DeviceRoute::device_route_for(&inv(0xb025, None), DIRECT_DEVICE_INDEX);
assert!(matches!(
route,
Some(DeviceRoute::Direct {
vendor_id: 0x046d,
product_id: 0xb025
})
));
}
#[test]
fn device_route_for_none_when_no_uid_and_non_direct_slot() {
let route = DeviceRoute::device_route_for(&inv(0xc52b, None), 1);
assert!(route.is_none());
}
#[test]
fn unifying_device_index_is_the_slot() {
let route = DeviceRoute::Unifying {
receiver_uid: "X".into(),
slot: 4,
};
assert_eq!(route.device_index(), 4);
}
#[test]
fn unifying_display_matches_bolt_format() {
let r = DeviceRoute::Unifying {
receiver_uid: "AABBCC".into(),
slot: 3,
};
assert_eq!(r.to_string(), "slot 3 on receiver AABBCC");
}
}