1use std::collections::HashMap;
43
44use crate::NodeId;
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum DevicePattern {
49 WearTak,
51 WearOs,
53 Hive,
55 Unknown,
57}
58
59impl DevicePattern {
60 pub fn rotates_addresses(&self) -> bool {
62 matches!(self, DevicePattern::WearTak | DevicePattern::WearOs)
63 }
64}
65
66pub fn detect_device_pattern(name: &str) -> DevicePattern {
68 if name.starts_with("WT-WEAROS-") {
69 DevicePattern::WearTak
70 } else if name.starts_with("WEAROS-") {
71 DevicePattern::WearOs
72 } else if name.starts_with("HIVE_") || name.starts_with("HIVE-") {
73 DevicePattern::Hive
74 } else {
75 DevicePattern::Unknown
76 }
77}
78
79pub fn is_weartak_device(name: &str) -> bool {
81 name.starts_with("WT-WEAROS-") || name.starts_with("WEAROS-")
82}
83
84pub fn normalize_weartak_name(name: &str) -> &str {
88 name.strip_prefix("WT-").unwrap_or(name)
89}
90
91#[derive(Debug, Clone)]
93pub struct DeviceLookupResult {
94 pub node_id: NodeId,
96 pub current_address: String,
98 pub address_changed: bool,
100 pub previous_address: Option<String>,
102}
103
104#[derive(Debug, Default)]
109pub struct AddressRotationHandler {
110 name_to_node: HashMap<String, NodeId>,
112 node_to_name: HashMap<NodeId, String>,
114 node_to_address: HashMap<NodeId, String>,
116 address_to_node: HashMap<String, NodeId>,
118}
119
120impl AddressRotationHandler {
121 pub fn new() -> Self {
123 Self::default()
124 }
125
126 pub fn register_device(&mut self, name: &str, address: &str, node_id: NodeId) {
130 if !name.is_empty() {
132 self.name_to_node.insert(name.to_string(), node_id);
133 self.node_to_name.insert(node_id, name.to_string());
134 }
135
136 self.address_to_node.insert(address.to_string(), node_id);
138 self.node_to_address.insert(node_id, address.to_string());
139
140 log::debug!(
141 "Registered device: name='{}' address='{}' node={:?}",
142 name,
143 address,
144 node_id
145 );
146 }
147
148 pub fn lookup_by_name(&self, name: &str) -> Option<NodeId> {
152 self.name_to_node.get(name).copied()
153 }
154
155 pub fn lookup_by_address(&self, address: &str) -> Option<NodeId> {
159 self.address_to_node.get(address).copied()
160 }
161
162 pub fn get_address(&self, node_id: &NodeId) -> Option<&String> {
164 self.node_to_address.get(node_id)
165 }
166
167 pub fn get_name(&self, node_id: &NodeId) -> Option<&String> {
169 self.node_to_name.get(node_id)
170 }
171
172 pub fn on_device_discovered(
182 &mut self,
183 name: &str,
184 address: &str,
185 ) -> Option<DeviceLookupResult> {
186 if !name.is_empty() {
188 if let Some(node_id) = self.name_to_node.get(name).copied() {
189 let current_address = self.node_to_address.get(&node_id).cloned();
190 let address_changed = current_address.as_ref() != Some(&address.to_string());
191 let previous_address = if address_changed {
192 current_address.clone()
193 } else {
194 None
195 };
196
197 if address_changed {
199 self.update_address_internal(node_id, address, current_address.as_deref());
200 }
201
202 return Some(DeviceLookupResult {
203 node_id,
204 current_address: address.to_string(),
205 address_changed,
206 previous_address,
207 });
208 }
209 }
210
211 if let Some(node_id) = self.address_to_node.get(address).copied() {
213 return Some(DeviceLookupResult {
214 node_id,
215 current_address: address.to_string(),
216 address_changed: false,
217 previous_address: None,
218 });
219 }
220
221 None
223 }
224
225 pub fn update_address(&mut self, name: &str, new_address: &str) -> bool {
227 if let Some(node_id) = self.name_to_node.get(name).copied() {
228 let old_address = self.node_to_address.get(&node_id).cloned();
229 self.update_address_internal(node_id, new_address, old_address.as_deref());
230 true
231 } else {
232 false
233 }
234 }
235
236 fn update_address_internal(
238 &mut self,
239 node_id: NodeId,
240 new_address: &str,
241 old_address: Option<&str>,
242 ) {
243 if let Some(old) = old_address {
245 self.address_to_node.remove(old);
246 log::info!(
247 "Address rotation detected for {:?}: {} -> {}",
248 node_id,
249 old,
250 new_address
251 );
252 }
253
254 self.address_to_node
256 .insert(new_address.to_string(), node_id);
257 self.node_to_address
258 .insert(node_id, new_address.to_string());
259 }
260
261 pub fn update_name(&mut self, node_id: NodeId, new_name: &str) {
263 if let Some(old_name) = self.node_to_name.get(&node_id).cloned() {
265 if old_name != new_name {
266 self.name_to_node.remove(&old_name);
267 log::debug!(
268 "Name updated for {:?}: '{}' -> '{}'",
269 node_id,
270 old_name,
271 new_name
272 );
273 }
274 }
275
276 if !new_name.is_empty() {
278 self.name_to_node.insert(new_name.to_string(), node_id);
279 self.node_to_name.insert(node_id, new_name.to_string());
280 }
281 }
282
283 pub fn remove_device(&mut self, node_id: &NodeId) {
285 if let Some(name) = self.node_to_name.remove(node_id) {
287 self.name_to_node.remove(&name);
288 }
289
290 if let Some(address) = self.node_to_address.remove(node_id) {
292 self.address_to_node.remove(&address);
293 }
294
295 log::debug!("Removed device {:?} from rotation handler", node_id);
296 }
297
298 pub fn clear(&mut self) {
300 self.name_to_node.clear();
301 self.node_to_name.clear();
302 self.node_to_address.clear();
303 self.address_to_node.clear();
304 }
305
306 pub fn device_count(&self) -> usize {
308 self.node_to_address.len()
309 }
310
311 pub fn stats(&self) -> AddressRotationStats {
313 AddressRotationStats {
314 devices_with_names: self.name_to_node.len(),
315 total_devices: self.node_to_address.len(),
316 address_mappings: self.address_to_node.len(),
317 }
318 }
319}
320
321#[derive(Debug, Clone, Copy)]
323pub struct AddressRotationStats {
324 pub devices_with_names: usize,
326 pub total_devices: usize,
328 pub address_mappings: usize,
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn test_device_pattern_detection() {
338 assert_eq!(
339 detect_device_pattern("WT-WEAROS-ABCD"),
340 DevicePattern::WearTak
341 );
342 assert_eq!(detect_device_pattern("WEAROS-1234"), DevicePattern::WearOs);
343 assert_eq!(
344 detect_device_pattern("HIVE_MESH-12345678"),
345 DevicePattern::Hive
346 );
347 assert_eq!(detect_device_pattern("HIVE-12345678"), DevicePattern::Hive);
348 assert_eq!(
349 detect_device_pattern("SomeOtherDevice"),
350 DevicePattern::Unknown
351 );
352 }
353
354 #[test]
355 fn test_weartak_detection() {
356 assert!(is_weartak_device("WT-WEAROS-ABCD"));
357 assert!(is_weartak_device("WEAROS-1234"));
358 assert!(!is_weartak_device("HIVE-12345678"));
359 }
360
361 #[test]
362 fn test_normalize_weartak_name() {
363 assert_eq!(normalize_weartak_name("WT-WEAROS-ABCD"), "WEAROS-ABCD");
364 assert_eq!(normalize_weartak_name("WEAROS-1234"), "WEAROS-1234");
365 }
366
367 #[test]
368 fn test_register_and_lookup() {
369 let mut handler = AddressRotationHandler::new();
370 let node_id = NodeId::new(0x12345678);
371
372 handler.register_device("WEAROS-ABCD", "AA:BB:CC:DD:EE:01", node_id);
373
374 assert_eq!(handler.lookup_by_name("WEAROS-ABCD"), Some(node_id));
375 assert_eq!(
376 handler.lookup_by_address("AA:BB:CC:DD:EE:01"),
377 Some(node_id)
378 );
379 assert_eq!(
380 handler.get_address(&node_id),
381 Some(&"AA:BB:CC:DD:EE:01".to_string())
382 );
383 }
384
385 #[test]
386 fn test_address_rotation_detection() {
387 let mut handler = AddressRotationHandler::new();
388 let node_id = NodeId::new(0x12345678);
389
390 handler.register_device("WEAROS-ABCD", "AA:BB:CC:DD:EE:01", node_id);
392
393 let result = handler
395 .on_device_discovered("WEAROS-ABCD", "AA:BB:CC:DD:EE:02")
396 .unwrap();
397
398 assert_eq!(result.node_id, node_id);
399 assert!(result.address_changed);
400 assert_eq!(
401 result.previous_address,
402 Some("AA:BB:CC:DD:EE:01".to_string())
403 );
404 assert_eq!(result.current_address, "AA:BB:CC:DD:EE:02");
405
406 assert_eq!(
408 handler.lookup_by_address("AA:BB:CC:DD:EE:02"),
409 Some(node_id)
410 );
411 assert_eq!(handler.lookup_by_address("AA:BB:CC:DD:EE:01"), None);
412 }
413
414 #[test]
415 fn test_new_device_discovery() {
416 let mut handler = AddressRotationHandler::new();
417
418 let result = handler.on_device_discovered("WEAROS-NEW", "AA:BB:CC:DD:EE:FF");
420 assert!(result.is_none());
421 }
422
423 #[test]
424 fn test_remove_device() {
425 let mut handler = AddressRotationHandler::new();
426 let node_id = NodeId::new(0x12345678);
427
428 handler.register_device("WEAROS-ABCD", "AA:BB:CC:DD:EE:01", node_id);
429 assert_eq!(handler.device_count(), 1);
430
431 handler.remove_device(&node_id);
432
433 assert_eq!(handler.device_count(), 0);
434 assert!(handler.lookup_by_name("WEAROS-ABCD").is_none());
435 assert!(handler.lookup_by_address("AA:BB:CC:DD:EE:01").is_none());
436 }
437
438 #[test]
439 fn test_update_name() {
440 let mut handler = AddressRotationHandler::new();
441 let node_id = NodeId::new(0x12345678);
442
443 handler.register_device("WEAROS-ABCD", "AA:BB:CC:DD:EE:01", node_id);
444 handler.update_name(node_id, "MyCallsign");
445
446 assert!(handler.lookup_by_name("WEAROS-ABCD").is_none());
447 assert_eq!(handler.lookup_by_name("MyCallsign"), Some(node_id));
448 }
449
450 #[test]
453 fn test_name_based_rotation_detection() {
454 let mut handler = AddressRotationHandler::new();
456 let node_id = NodeId::new(0xAABBCCDD);
457
458 handler.register_device("WEAROS-1234", "AA:BB:CC:DD:EE:01", node_id);
460
461 let result = handler
463 .on_device_discovered("WEAROS-1234", "AA:BB:CC:DD:EE:02")
464 .unwrap();
465
466 assert_eq!(result.node_id, node_id);
467 assert!(result.address_changed);
468 assert_eq!(
469 result.previous_address,
470 Some("AA:BB:CC:DD:EE:01".to_string())
471 );
472 assert_eq!(result.current_address, "AA:BB:CC:DD:EE:02");
473
474 assert!(handler.lookup_by_address("AA:BB:CC:DD:EE:01").is_none());
476 assert_eq!(
478 handler.lookup_by_address("AA:BB:CC:DD:EE:02"),
479 Some(node_id)
480 );
481 assert_eq!(handler.lookup_by_name("WEAROS-1234"), Some(node_id));
483 }
484
485 #[test]
486 fn test_remove_device_cleans_all_maps() {
487 let mut handler = AddressRotationHandler::new();
489 let node_id = NodeId::new(0x12345678);
490
491 handler.register_device("WEAROS-ABCD", "AA:BB:CC:DD:EE:01", node_id);
492
493 assert_eq!(handler.lookup_by_name("WEAROS-ABCD"), Some(node_id));
495 assert_eq!(
496 handler.lookup_by_address("AA:BB:CC:DD:EE:01"),
497 Some(node_id)
498 );
499 assert_eq!(
500 handler.get_address(&node_id),
501 Some(&"AA:BB:CC:DD:EE:01".to_string())
502 );
503 assert_eq!(handler.get_name(&node_id), Some(&"WEAROS-ABCD".to_string()));
504 assert_eq!(handler.device_count(), 1);
505
506 handler.remove_device(&node_id);
508
509 assert!(handler.lookup_by_name("WEAROS-ABCD").is_none());
511 assert!(handler.lookup_by_address("AA:BB:CC:DD:EE:01").is_none());
512 assert!(handler.get_address(&node_id).is_none());
513 assert!(handler.get_name(&node_id).is_none());
514 assert_eq!(handler.device_count(), 0);
515
516 let stats = handler.stats();
518 assert_eq!(stats.devices_with_names, 0);
519 assert_eq!(stats.total_devices, 0);
520 assert_eq!(stats.address_mappings, 0);
521 }
522}