lighthouse_manager/
protocol.rs1use crate::lighthouse::{Lighthouse, LighthouseVersion};
3use anyhow::{Result, anyhow};
4
5pub fn build_v1_power_on(id: &str) -> Result<Vec<u8>> {
12 validate_v1_id(id)?;
13 let id_bytes = parse_v1_id_bytes(id);
14 let mut cmd = vec![0x12, 0x00, 0x00, 0x00];
15 let mut rev_id = id_bytes.clone();
16 rev_id.reverse();
17 cmd.extend_from_slice(&rev_id); cmd.resize(20, 0x00); Ok(cmd)
20}
21
22pub fn build_v1_sleep(id: &str) -> Result<Vec<u8>> {
29 validate_v1_id(id)?;
30 let id_bytes = parse_v1_id_bytes(id);
31 let mut cmd = vec![0x12, 0x02, 0x00, 0x01];
32 let mut rev_id = id_bytes.clone();
33 rev_id.reverse();
34 cmd.extend_from_slice(&rev_id); cmd.resize(20, 0x00); Ok(cmd)
37}
38
39#[must_use]
41pub fn build_v2_power_on() -> Vec<u8> {
42 vec![0x01]
43}
44
45#[must_use]
47pub fn build_v2_sleep() -> Vec<u8> {
48 vec![0x00]
49}
50
51#[must_use]
53pub fn build_v2_identify() -> Vec<u8> {
54 vec![0x01]
55}
56
57fn validate_v1_id(id: &str) -> Result<()> {
59 if id.len() != 8 {
60 return Err(anyhow!("Invalid V1 ID length: {id} (expected 8 chars)"));
61 }
62 if !id.chars().all(|c| c.is_ascii_hexdigit()) {
63 return Err(anyhow!("V1 ID contains non-hex characters: {id}"));
64 }
65 Ok(())
66}
67
68fn parse_v1_id_bytes(id: &str) -> Vec<u8> {
71 (0..id.len())
72 .step_by(2)
73 .map(|i| u8::from_str_radix(&id[i..i + 2], 16).unwrap())
74 .collect()
75}
76
77pub fn build_power_command(lh: &Lighthouse) -> Result<Vec<u8>> {
83 match lh.version() {
84 LighthouseVersion::V1 => {
85 let id = lh
86 .id
87 .as_ref()
88 .ok_or_else(|| anyhow!("V1 lighthouse missing ID for power command"))?;
89 build_v1_power_on(id)
90 }
91 LighthouseVersion::V2 => Ok(build_v2_power_on()),
92 }
93}
94
95pub fn build_sleep_command(lh: &Lighthouse) -> Result<Vec<u8>> {
101 match lh.version() {
102 LighthouseVersion::V1 => {
103 let id = lh
104 .id
105 .as_ref()
106 .ok_or_else(|| anyhow!("V1 lighthouse missing ID for sleep command"))?;
107 build_v1_sleep(id)
108 }
109 LighthouseVersion::V2 => Ok(build_v2_sleep()),
110 }
111}
112
113pub fn build_identify_command(lh: &Lighthouse) -> Result<Vec<u8>> {
119 match lh.version() {
120 LighthouseVersion::V2 => Ok(build_v2_identify()),
121 LighthouseVersion::V1 => Err(anyhow!("Identify is not supported on V1 lighthouses")),
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn test_v1_power_on_command() {
131 let cmd = build_v1_power_on("AABBCCDD").unwrap();
132 assert_eq!(cmd.len(), 20);
133 assert_eq!(cmd[0], 0x12);
134 assert_eq!(cmd[1], 0x00);
135 assert_eq!(cmd[2], 0x00);
136 assert_eq!(cmd[3], 0x00);
137 assert_eq!(&cmd[4..8], &[0xDD, 0xCC, 0xBB, 0xAA]);
139 assert_eq!(&cmd[8..20], &[0u8; 12]);
141 }
142
143 #[test]
144 fn test_v1_sleep_command() {
145 let cmd = build_v1_sleep("AABBCCDD").unwrap();
146 assert_eq!(cmd.len(), 20);
147 assert_eq!(cmd[0], 0x12);
148 assert_eq!(cmd[1], 0x02);
149 assert_eq!(cmd[2], 0x00);
150 assert_eq!(cmd[3], 0x01);
151 assert_eq!(&cmd[4..8], &[0xDD, 0xCC, 0xBB, 0xAA]);
153 }
154
155 #[test]
156 fn test_v2_commands() {
157 assert_eq!(build_v2_power_on(), vec![0x01]);
158 assert_eq!(build_v2_sleep(), vec![0x00]);
159 assert_eq!(build_v2_identify(), vec![0x01]);
160 }
161
162 #[test]
163 fn test_validate_invalid_id() {
164 assert!(build_v1_power_on("12345").is_err()); assert!(build_v1_power_on("GGHHIIJJ").is_err()); assert!(build_v1_power_on("AABBCCDD11").is_err()); }
168
169 #[test]
170 fn test_parse_v1_id_bytes() {
171 let bytes = parse_v1_id_bytes("AABBCCDD");
172 assert_eq!(bytes, vec![0xAA, 0xBB, 0xCC, 0xDD]);
173 }
174
175 #[test]
176 fn test_build_power_command_v1() {
177 let lh = Lighthouse {
178 name: "HTC BS-AABBCCDD".into(),
179 address: "AA:BB:CC:DD:EE:FF".into(),
180 id: Some("AABBCCDD".into()),
181 managed: true,
182 };
183 let cmd = build_power_command(&lh).unwrap();
184 assert_eq!(cmd.len(), 20);
185 assert_eq!(cmd, build_v1_power_on("AABBCCDD").unwrap());
187 }
188
189 #[test]
190 fn test_build_power_command_v2() {
191 let lh = Lighthouse {
192 name: "LHB-0A1B2C3D".into(),
193 address: "11:22:33:44:55:66".into(),
194 id: None,
195 managed: true,
196 };
197 let cmd = build_power_command(&lh).unwrap();
198 assert_eq!(cmd, vec![0x01]);
199 }
200
201 #[test]
202 fn test_build_sleep_command_v2() {
203 let lh = Lighthouse {
204 name: "LHB-0A1B2C3D".into(),
205 address: "11:22:33:44:55:66".into(),
206 id: None,
207 managed: true,
208 };
209 let cmd = build_sleep_command(&lh).unwrap();
210 assert_eq!(cmd, vec![0x00]);
211 }
212
213 #[test]
214 fn test_identify_v1_fails() {
215 let lh = Lighthouse {
216 name: "HTC BS-AABBCCDD".into(),
217 address: "AA:BB:CC:DD:EE:FF".into(),
218 id: Some("AABBCCDD".into()),
219 managed: true,
220 };
221 assert!(build_identify_command(&lh).is_err());
222 }
223
224 #[test]
225 fn test_build_power_command_missing_id() {
226 let lh = Lighthouse {
227 name: "HTC BS-AABBCCDD".into(),
228 address: "AA:BB:CC:DD:EE:FF".into(),
229 id: None,
230 managed: true,
231 };
232 assert!(build_power_command(&lh).is_err());
233 }
234}