Skip to main content

lighthouse_manager/
lighthouse.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4/// Version of a Lighthouse base station.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6pub enum LighthouseVersion {
7    V1, // HTC BS-*
8    V2, // LHB-*
9}
10
11impl fmt::Display for LighthouseVersion {
12    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13        match self {
14            LighthouseVersion::V1 => write!(f, "V1 (HTC BS)"),
15            LighthouseVersion::V2 => write!(f, "V2 (LHB)"),
16        }
17    }
18}
19
20/// A discovered or known Lighthouse base station.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct Lighthouse {
23    /// Friendly name of the device (e.g. "HTC BS-AABBCCDD" or "LHB-0A1B2C3D").
24    pub name: String,
25
26    /// Bluetooth MAC address in colon-separated hex format: "AA:BB:CC:DD:EE:FF".
27    pub address: String,
28
29    /// 8-char hex ID printed on the back of V1 lighthouses (e.g. "AABBCCDD").
30    /// Only present for V1 devices and required for power control.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub id: Option<String>,
33
34    /// Whether this lighthouse should be controlled by this manager.
35    /// Newly discovered lighthouses are unmanaged by default.
36    #[serde(default)]
37    pub managed: bool,
38}
39
40impl Lighthouse {
41    /// Determine the version of a lighthouse from its name.
42    /// - Starts with "HTC BS" → V1
43    /// - Starts with "LHB-"   → V2
44    #[must_use]
45    pub fn version(&self) -> LighthouseVersion {
46        if self.name.starts_with("LHB-") {
47            LighthouseVersion::V2
48        } else {
49            LighthouseVersion::V1
50        }
51    }
52
53    /// Returns the GATT characteristic UUID for power-on/sleep commands.
54    #[must_use]
55    pub fn power_characteristic(&self) -> &'static str {
56        match self.version() {
57            LighthouseVersion::V1 => "0000cb01-0000-1000-8000-00805f9b34fb",
58            LighthouseVersion::V2 => "00001525-1212-efde-1523-785feabcd124",
59        }
60    }
61
62    /// Returns the GATT characteristic UUID for identify commands (V2 only).
63    #[must_use]
64    pub fn identify_characteristic(&self) -> Option<&'static str> {
65        match self.version() {
66            LighthouseVersion::V2 => Some("00008421-1212-efde-1523-785feabcd124"),
67            LighthouseVersion::V1 => None, // V1 doesn't support identify
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn test_v1_version_detection() {
78        let lh = Lighthouse {
79            name: "HTC BS-AABBCCDD".into(),
80            address: "AA:BB:CC:DD:EE:FF".into(),
81            id: None,
82            managed: false,
83        };
84        assert_eq!(lh.version(), LighthouseVersion::V1);
85    }
86
87    #[test]
88    fn test_v2_version_detection() {
89        let lh = Lighthouse {
90            name: "LHB-0A1B2C3D".into(),
91            address: "11:22:33:44:55:66".into(),
92            id: None,
93            managed: false,
94        };
95        assert_eq!(lh.version(), LighthouseVersion::V2);
96    }
97
98    #[test]
99    fn test_serde_roundtrip() {
100        let lh = Lighthouse {
101            name: "LHB-0A1B2C3D".into(),
102            address: "AA:BB:CC:DD:EE:FF".into(),
103            id: None,
104            managed: true,
105        };
106        let json = serde_json::to_string(&lh).unwrap();
107        let restored: Lighthouse = serde_json::from_str(&json).unwrap();
108        assert_eq!(restored, lh);
109
110        let v1_lh = Lighthouse {
111            name: "HTC BS-AABBCCDD".into(),
112            address: "11:22:33:44:55:66".into(),
113            id: Some("AABBCCDD".into()),
114            managed: true,
115        };
116        let v1_json = serde_json::to_string(&v1_lh).unwrap();
117        assert!(v1_json.contains("\"id\":\"AABBCCDD\""));
118        let restored_v1: Lighthouse = serde_json::from_str(&v1_json).unwrap();
119        assert_eq!(restored_v1, v1_lh);
120    }
121
122    #[test]
123    fn test_database_roundtrip() {
124        use crate::storage;
125        let lh1 = Lighthouse {
126            name: "HTC BS-AABBCCDD".into(),
127            address: "AA:BB:CC:DD:EE:FF".into(),
128            id: Some("AABBCCDD".into()),
129            managed: true,
130        };
131        let lh2 = Lighthouse {
132            name: "LHB-0A1B2C3D".into(),
133            address: "11:22:33:44:55:66".into(),
134            id: None,
135            managed: true,
136        };
137        let settings = storage::AppSettings {
138            version: 1,
139            lighthouses: vec![lh1.clone(), lh2.clone()],
140            ..Default::default()
141        };
142        let json = serde_json::to_string_pretty(&settings).unwrap();
143        let restored: storage::AppSettings = serde_json::from_str(&json).unwrap();
144        assert_eq!(restored.lighthouses, settings.lighthouses);
145    }
146}