use crate::driver::{
Device, DeviceList, DeviceListChangeType, DeviceType, DjDev, dj_dev::Oneofdev,
};
use crate::profiles::{CommandSet, DeviceProfile, NJ98_CP_V4, SUPPORTED_PROFILES};
pub trait DeviceCatalog: Send + Sync + 'static {
fn initial_snapshot(&self) -> DeviceList;
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct HidInterface {
path: String,
vendor_id: u16,
product_id: u16,
usage_page: u16,
usage: u16,
interface_number: i32,
}
pub struct HidCatalog {
snapshot: DeviceList,
devices: Vec<DetectedDevice>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct DetectedDevice {
path: String,
profile: &'static DeviceProfile,
}
impl HidCatalog {
pub fn enumerate() -> Result<Self, hidapi::HidError> {
let api = hidapi::HidApi::new()?;
let interfaces = api
.device_list()
.map(HidInterface::from)
.collect::<Vec<_>>();
let devices = matching_interfaces(&interfaces)
.map(|(profile, interface)| DetectedDevice {
path: interface.path.clone(),
profile,
})
.collect();
Ok(Self {
snapshot: snapshot_from_interfaces(&interfaces),
devices,
})
}
pub fn vendor_paths(&self) -> Vec<String> {
self.devices
.iter()
.map(|device| device.path.clone())
.collect()
}
pub fn is_empty(&self) -> bool {
self.devices.is_empty()
}
pub fn transport_devices(&self) -> Vec<(String, CommandSet)> {
self.devices
.iter()
.map(|device| (device.path.clone(), device.profile.command_set))
.collect()
}
}
impl DeviceCatalog for HidCatalog {
fn initial_snapshot(&self) -> DeviceList {
self.snapshot.clone()
}
}
#[derive(Default)]
pub struct SimulatedCatalog;
impl DeviceCatalog for SimulatedCatalog {
fn initial_snapshot(&self) -> DeviceList {
snapshot_with_devices(vec![device("mock://3151/5030".into(), &NJ98_CP_V4)])
}
}
impl From<&hidapi::DeviceInfo> for HidInterface {
fn from(info: &hidapi::DeviceInfo) -> Self {
Self {
path: info.path().to_string_lossy().into_owned(),
vendor_id: info.vendor_id(),
product_id: info.product_id(),
usage_page: info.usage_page(),
usage: info.usage(),
interface_number: info.interface_number(),
}
}
}
fn snapshot_from_interfaces(interfaces: &[HidInterface]) -> DeviceList {
let matched = matching_interfaces(interfaces).collect::<Vec<_>>();
let has_wired = matched.iter().any(|(profile, _)| !profile.is_wireless);
let devices = matched
.into_iter()
.filter(|(profile, _)| !has_wired || !profile.is_wireless)
.map(|(profile, interface)| device(interface.path.clone(), profile))
.collect();
snapshot_with_devices(devices)
}
fn matching_interfaces(
interfaces: &[HidInterface],
) -> impl Iterator<Item = (&'static DeviceProfile, &HidInterface)> {
SUPPORTED_PROFILES.iter().flat_map(|profile| {
interfaces
.iter()
.filter(move |interface| profile_matches(profile, interface))
.map(move |interface| (profile, interface))
})
}
fn profile_matches(profile: &DeviceProfile, interface: &HidInterface) -> bool {
interface.vendor_id == profile.vendor_id
&& interface.product_id == profile.product_id
&& interface.usage_page == profile.usage_page
&& interface.usage == profile.usage
&& interface.interface_number == profile.interface_number
}
fn device(path: String, profile: &DeviceProfile) -> DjDev {
DjDev {
oneofdev: Some(Oneofdev::Dev(Device {
devtype: DeviceType::Yzwkeyboard.into(),
is24: profile.is_wireless,
path,
id: profile.web_profile_id,
battery: 100,
isonline: true,
vid: profile.vendor_id.into(),
pid: profile.product_id.into(),
})),
}
}
fn snapshot_with_devices(devlist: Vec<DjDev>) -> DeviceList {
DeviceList {
devlist,
r#type: DeviceListChangeType::Init.into(),
}
}
#[cfg(test)]
mod tests {
use super::{DeviceCatalog, HidInterface, SimulatedCatalog, snapshot_from_interfaces};
use crate::driver::dj_dev::Oneofdev;
#[test]
fn snapshot_identifies_wired_nj98_cp_v4() {
let snapshot = SimulatedCatalog.initial_snapshot();
assert_eq!(snapshot.devlist.len(), 1);
let Some(Oneofdev::Dev(device)) = &snapshot.devlist[0].oneofdev else {
panic!("expected direct device");
};
assert_eq!((device.vid, device.pid), (0x3151, 0x5030));
assert_eq!(device.id, 3496);
assert!(device.isonline);
assert!(!device.is24);
}
#[test]
fn real_snapshot_prefers_wired_vendor_interface() {
let expected = HidInterface {
path: "/dev/hidraw2".into(),
vendor_id: 0x3151,
product_id: 0x5030,
usage_page: 0xffff,
usage: 2,
interface_number: 2,
};
let mut wrong_interface = expected.clone();
wrong_interface.path = "/dev/hidraw1".into();
wrong_interface.interface_number = 1;
let receiver = HidInterface {
path: "/dev/hidraw5".into(),
vendor_id: 0x3151,
product_id: 0x5038,
usage_page: 0xffff,
usage: 2,
interface_number: 2,
};
let snapshot = snapshot_from_interfaces(&[wrong_interface, receiver, expected]);
assert_eq!(snapshot.devlist.len(), 1);
let Some(Oneofdev::Dev(device)) = &snapshot.devlist[0].oneofdev else {
panic!("expected direct device");
};
assert_eq!(device.path, "/dev/hidraw2");
assert_eq!((device.vid, device.pid), (0x3151, 0x5030));
assert!(!device.is24);
}
#[test]
fn real_snapshot_selects_wireless_vendor_interface_without_wired_device() {
let receiver = HidInterface {
path: "/dev/hidraw5".into(),
vendor_id: 0x3151,
product_id: 0x5038,
usage_page: 0xffff,
usage: 2,
interface_number: 2,
};
let snapshot = snapshot_from_interfaces(&[receiver]);
assert_eq!(snapshot.devlist.len(), 1);
let Some(Oneofdev::Dev(device)) = &snapshot.devlist[0].oneofdev else {
panic!("expected receiver device");
};
assert_eq!(device.path, "/dev/hidraw5");
assert_eq!((device.vid, device.pid), (0x3151, 0x5038));
assert!(device.is24);
}
}