use std::fmt;
use serde::{Deserialize, Serialize};
use crate::device::DeviceInventory;
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,
},
RawHid {
vendor_id: u16,
product_id: u16,
usage_page: u16,
usage_id: u16,
identity: String,
},
}
pub const BOLT_PIDS: &[u16] = &[0xc548];
pub const UNIFYING_PIDS: &[u16] = &[0xc52b, 0xc532, 0xc539];
pub const LIGHTSPEED_PIDS: &[u16] = &[0xc53f, 0xc547];
#[must_use]
pub fn speaks_unifying_protocol(product_id: u16) -> bool {
UNIFYING_PIDS.contains(&product_id) || LIGHTSPEED_PIDS.contains(&product_id)
}
#[must_use]
pub fn is_receiver_pid(product_id: u16) -> bool {
BOLT_PIDS.contains(&product_id) || speaks_unifying_protocol(product_id)
}
#[must_use]
pub fn receiver_display_name(product_id: u16) -> &'static str {
if LIGHTSPEED_PIDS.contains(&product_id) {
"Lightspeed Receiver"
} else {
"Unifying Receiver"
}
}
impl DeviceRoute {
#[must_use]
pub fn shares_transport(&self, other: &Self) -> bool {
match (self, other) {
(
Self::Bolt {
receiver_uid: left, ..
},
Self::Bolt {
receiver_uid: right,
..
},
)
| (
Self::Unifying {
receiver_uid: left, ..
},
Self::Unifying {
receiver_uid: right,
..
},
) => left.eq_ignore_ascii_case(right),
_ => false,
}
}
#[must_use]
pub fn device_index(&self) -> u8 {
match self {
Self::Bolt { slot, .. } | Self::Unifying { slot, .. } => *slot,
Self::Direct { .. } | Self::RawHid { .. } => DIRECT_DEVICE_INDEX,
}
}
#[must_use]
pub fn device_route_for(inv: &DeviceInventory, slot: u8) -> Option<Self> {
match &inv.receiver.unique_id {
Some(uid) if speaks_unifying_protocol(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}"),
Self::RawHid {
vendor_id,
product_id,
usage_page,
usage_id,
identity,
} => write!(
f,
"raw {vendor_id:04x}:{product_id:04x} usage {usage_page:04x}:{usage_id:04x} ({identity})"
),
}
}
}
#[cfg(test)]
mod tests {
use std::assert_matches;
use crate::device::{DeviceInventory, ReceiverInfo};
use super::{
DIRECT_DEVICE_INDEX, DeviceRoute, LIGHTSPEED_PIDS, UNIFYING_PIDS, receiver_display_name,
};
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_lightspeed_pids_create_unifying_route() {
for &pid in LIGHTSPEED_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"),
"lightspeed pid {pid:#06x} should produce a Unifying route"
);
}
}
#[test]
fn lightspeed_receiver_has_its_own_display_name() {
assert_eq!(receiver_display_name(0xc53f), "Lightspeed Receiver");
assert_eq!(receiver_display_name(0xc547), "Lightspeed Receiver");
assert_eq!(receiver_display_name(0xc52b), "Unifying Receiver");
}
#[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");
}
}