use serde::{Deserialize, Serialize}; use crate::device::{Device, DeviceType, get_device_types}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Repo { #[serde(default)] hr_sensors: Vec, #[serde(default)] cad_sensors: Vec, #[serde(default)] trainers: Vec, } impl Repo { pub fn new() -> Self { Self { hr_sensors: Vec::new(), cad_sensors: Vec::new(), trainers: Vec::new(), } } } pub fn repo_from_devices(devices: Vec) -> Repo { let mut repo = Repo::new(); for dev in devices.into_iter() { let types = get_device_types(&dev); for t in types { match t { DeviceType::HrSensor => repo.hr_sensors.push(dev.clone()), DeviceType::CadSensor => repo.cad_sensors.push(dev.clone()), DeviceType::Trainer => repo.trainers.push(dev.clone()), } } } fn sort_by_rssi_desc(v: &mut [Device]) { v.sort_by(|a, b| { let ra = a.rssi; let rb = b.rssi; match (ra, rb) { (Some(va), Some(vb)) => vb.cmp(&va), (Some(_), None) => std::cmp::Ordering::Less, (None, Some(_)) => std::cmp::Ordering::Greater, (None, None) => std::cmp::Ordering::Equal, } }); } sort_by_rssi_desc(&mut repo.hr_sensors); sort_by_rssi_desc(&mut repo.cad_sensors); sort_by_rssi_desc(&mut repo.trainers); repo } #[cfg(test)] mod tests { use super::*; /// Helper: a cadence device (CSC service 0x1816) with the given RSSI. fn cadence_device(rssi: Option) -> Device { Device { id: format!("cad-{rssi:?}"), address: "00:00:00:00:00:00".into(), display_name: None, rssi, service_uuids: vec!["1816".into()], characteristics: vec![], manufacturer_data: None, metadata: None, } } #[test] fn test_repo_classifies_magene_as_cadence_sensor() { let magene = Device { id: "hci0/dev_F1_F2_7C_77_14_BE".into(), address: "F1:F2:7C:77:14:BE".into(), display_name: Some("10911-17".into()), rssi: Some(-42), service_uuids: vec!["00001816-0000-1000-8000-00805f9b34fb".into()], characteristics: vec![], manufacturer_data: Some("6b009f2a110300030072".into()), metadata: None, }; let repo = repo_from_devices(vec![magene]); assert_eq!(repo.cad_sensors.len(), 1); assert!(repo.hr_sensors.is_empty()); assert!(repo.trainers.is_empty()); } #[test] fn test_sorts_devices_by_rssi_descending() { let devices = vec![ cadence_device(Some(-80)), cadence_device(Some(-42)), cadence_device(Some(-60)), ]; let repo = repo_from_devices(devices); let rssis: Vec<_> = repo.cad_sensors.iter().map(|d| d.rssi).collect(); assert_eq!(rssis, vec![Some(-42), Some(-60), Some(-80)]); } #[test] fn test_devices_without_rssi_go_last() { let devices = vec![cadence_device(None), cadence_device(Some(-50))]; let repo = repo_from_devices(devices); assert_eq!(repo.cad_sensors[0].rssi, Some(-50)); assert_eq!(repo.cad_sensors[1].rssi, None); } #[test] fn test_indoor_bike_lands_in_trainer_and_cadence_buckets() { let indoor_bike = Device { id: "indoor-bike".into(), address: "00:00:00:00:00:00".into(), display_name: None, rssi: Some(-70), service_uuids: vec![], characteristics: vec!["2ad2".into()], manufacturer_data: None, metadata: None, }; let repo = repo_from_devices(vec![indoor_bike]); assert_eq!(repo.trainers.len(), 1); assert_eq!(repo.cad_sensors.len(), 1); assert!(repo.hr_sensors.is_empty()); } }