keydous-bridge 0.1.2

Linux bridge for configuring Keydous keyboards with the official web driver
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);
    }
}