use std::collections::HashMap;
use crate::NodeId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DevicePattern {
WearTak,
WearOs,
Peat,
Unknown,
}
impl DevicePattern {
pub fn rotates_addresses(&self) -> bool {
matches!(self, DevicePattern::WearTak | DevicePattern::WearOs)
}
}
pub fn detect_device_pattern(name: &str) -> DevicePattern {
if name.starts_with("WT-WEAROS-") {
DevicePattern::WearTak
} else if name.starts_with("WEAROS-") {
DevicePattern::WearOs
} else if name.starts_with("PEAT_") || name.starts_with("PEAT-") {
DevicePattern::Peat
} else {
DevicePattern::Unknown
}
}
pub fn is_weartak_device(name: &str) -> bool {
name.starts_with("WT-WEAROS-") || name.starts_with("WEAROS-")
}
pub fn normalize_weartak_name(name: &str) -> &str {
name.strip_prefix("WT-").unwrap_or(name)
}
#[derive(Debug, Clone)]
pub struct DeviceLookupResult {
pub node_id: NodeId,
pub current_address: String,
pub address_changed: bool,
pub previous_address: Option<String>,
}
#[derive(Debug, Default)]
pub struct AddressRotationHandler {
name_to_node: HashMap<String, NodeId>,
node_to_name: HashMap<NodeId, String>,
node_to_address: HashMap<NodeId, String>,
address_to_node: HashMap<String, NodeId>,
}
impl AddressRotationHandler {
pub fn new() -> Self {
Self::default()
}
pub fn register_device(&mut self, name: &str, address: &str, node_id: NodeId) {
if !name.is_empty() {
self.name_to_node.insert(name.to_string(), node_id);
self.node_to_name.insert(node_id, name.to_string());
}
self.address_to_node.insert(address.to_string(), node_id);
self.node_to_address.insert(node_id, address.to_string());
log::debug!(
"Registered device: name='{}' address='{}' node={:?}",
name,
address,
node_id
);
}
pub fn lookup_by_name(&self, name: &str) -> Option<NodeId> {
self.name_to_node.get(name).copied()
}
pub fn lookup_by_address(&self, address: &str) -> Option<NodeId> {
self.address_to_node.get(address).copied()
}
pub fn get_address(&self, node_id: &NodeId) -> Option<&String> {
self.node_to_address.get(node_id)
}
pub fn get_name(&self, node_id: &NodeId) -> Option<&String> {
self.node_to_name.get(node_id)
}
pub fn on_device_discovered(
&mut self,
name: &str,
address: &str,
) -> Option<DeviceLookupResult> {
if !name.is_empty() {
if let Some(node_id) = self.name_to_node.get(name).copied() {
let current_address = self.node_to_address.get(&node_id).cloned();
let address_changed = current_address.as_ref() != Some(&address.to_string());
let previous_address = if address_changed {
current_address.clone()
} else {
None
};
if address_changed {
self.update_address_internal(node_id, address, current_address.as_deref());
}
return Some(DeviceLookupResult {
node_id,
current_address: address.to_string(),
address_changed,
previous_address,
});
}
}
if let Some(node_id) = self.address_to_node.get(address).copied() {
return Some(DeviceLookupResult {
node_id,
current_address: address.to_string(),
address_changed: false,
previous_address: None,
});
}
None
}
pub fn update_address(&mut self, name: &str, new_address: &str) -> bool {
if let Some(node_id) = self.name_to_node.get(name).copied() {
let old_address = self.node_to_address.get(&node_id).cloned();
self.update_address_internal(node_id, new_address, old_address.as_deref());
true
} else {
false
}
}
fn update_address_internal(
&mut self,
node_id: NodeId,
new_address: &str,
old_address: Option<&str>,
) {
if let Some(old) = old_address {
self.address_to_node.remove(old);
log::info!(
"Address rotation detected for {:?}: {} -> {}",
node_id,
old,
new_address
);
}
self.address_to_node
.insert(new_address.to_string(), node_id);
self.node_to_address
.insert(node_id, new_address.to_string());
}
pub fn update_name(&mut self, node_id: NodeId, new_name: &str) {
if let Some(old_name) = self.node_to_name.get(&node_id).cloned() {
if old_name != new_name {
self.name_to_node.remove(&old_name);
log::debug!(
"Name updated for {:?}: '{}' -> '{}'",
node_id,
old_name,
new_name
);
}
}
if !new_name.is_empty() {
self.name_to_node.insert(new_name.to_string(), node_id);
self.node_to_name.insert(node_id, new_name.to_string());
}
}
pub fn remove_device(&mut self, node_id: &NodeId) {
if let Some(name) = self.node_to_name.remove(node_id) {
self.name_to_node.remove(&name);
}
if let Some(address) = self.node_to_address.remove(node_id) {
self.address_to_node.remove(&address);
}
log::debug!("Removed device {:?} from rotation handler", node_id);
}
pub fn clear(&mut self) {
self.name_to_node.clear();
self.node_to_name.clear();
self.node_to_address.clear();
self.address_to_node.clear();
}
pub fn device_count(&self) -> usize {
self.node_to_address.len()
}
pub fn stats(&self) -> AddressRotationStats {
AddressRotationStats {
devices_with_names: self.name_to_node.len(),
total_devices: self.node_to_address.len(),
address_mappings: self.address_to_node.len(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct AddressRotationStats {
pub devices_with_names: usize,
pub total_devices: usize,
pub address_mappings: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_device_pattern_detection() {
assert_eq!(
detect_device_pattern("WT-WEAROS-ABCD"),
DevicePattern::WearTak
);
assert_eq!(detect_device_pattern("WEAROS-1234"), DevicePattern::WearOs);
assert_eq!(
detect_device_pattern("PEAT_MESH-12345678"),
DevicePattern::Peat
);
assert_eq!(detect_device_pattern("PEAT-12345678"), DevicePattern::Peat);
assert_eq!(
detect_device_pattern("SomeOtherDevice"),
DevicePattern::Unknown
);
}
#[test]
fn test_weartak_detection() {
assert!(is_weartak_device("WT-WEAROS-ABCD"));
assert!(is_weartak_device("WEAROS-1234"));
assert!(!is_weartak_device("PEAT-12345678"));
}
#[test]
fn test_normalize_weartak_name() {
assert_eq!(normalize_weartak_name("WT-WEAROS-ABCD"), "WEAROS-ABCD");
assert_eq!(normalize_weartak_name("WEAROS-1234"), "WEAROS-1234");
}
#[test]
fn test_register_and_lookup() {
let mut handler = AddressRotationHandler::new();
let node_id = NodeId::new(0x12345678);
handler.register_device("WEAROS-ABCD", "AA:BB:CC:DD:EE:01", node_id);
assert_eq!(handler.lookup_by_name("WEAROS-ABCD"), Some(node_id));
assert_eq!(
handler.lookup_by_address("AA:BB:CC:DD:EE:01"),
Some(node_id)
);
assert_eq!(
handler.get_address(&node_id),
Some(&"AA:BB:CC:DD:EE:01".to_string())
);
}
#[test]
fn test_address_rotation_detection() {
let mut handler = AddressRotationHandler::new();
let node_id = NodeId::new(0x12345678);
handler.register_device("WEAROS-ABCD", "AA:BB:CC:DD:EE:01", node_id);
let result = handler
.on_device_discovered("WEAROS-ABCD", "AA:BB:CC:DD:EE:02")
.unwrap();
assert_eq!(result.node_id, node_id);
assert!(result.address_changed);
assert_eq!(
result.previous_address,
Some("AA:BB:CC:DD:EE:01".to_string())
);
assert_eq!(result.current_address, "AA:BB:CC:DD:EE:02");
assert_eq!(
handler.lookup_by_address("AA:BB:CC:DD:EE:02"),
Some(node_id)
);
assert_eq!(handler.lookup_by_address("AA:BB:CC:DD:EE:01"), None);
}
#[test]
fn test_new_device_discovery() {
let mut handler = AddressRotationHandler::new();
let result = handler.on_device_discovered("WEAROS-NEW", "AA:BB:CC:DD:EE:FF");
assert!(result.is_none());
}
#[test]
fn test_remove_device() {
let mut handler = AddressRotationHandler::new();
let node_id = NodeId::new(0x12345678);
handler.register_device("WEAROS-ABCD", "AA:BB:CC:DD:EE:01", node_id);
assert_eq!(handler.device_count(), 1);
handler.remove_device(&node_id);
assert_eq!(handler.device_count(), 0);
assert!(handler.lookup_by_name("WEAROS-ABCD").is_none());
assert!(handler.lookup_by_address("AA:BB:CC:DD:EE:01").is_none());
}
#[test]
fn test_update_name() {
let mut handler = AddressRotationHandler::new();
let node_id = NodeId::new(0x12345678);
handler.register_device("WEAROS-ABCD", "AA:BB:CC:DD:EE:01", node_id);
handler.update_name(node_id, "MyCallsign");
assert!(handler.lookup_by_name("WEAROS-ABCD").is_none());
assert_eq!(handler.lookup_by_name("MyCallsign"), Some(node_id));
}
#[test]
fn test_name_based_rotation_detection() {
let mut handler = AddressRotationHandler::new();
let node_id = NodeId::new(0xAABBCCDD);
handler.register_device("WEAROS-1234", "AA:BB:CC:DD:EE:01", node_id);
let result = handler
.on_device_discovered("WEAROS-1234", "AA:BB:CC:DD:EE:02")
.unwrap();
assert_eq!(result.node_id, node_id);
assert!(result.address_changed);
assert_eq!(
result.previous_address,
Some("AA:BB:CC:DD:EE:01".to_string())
);
assert_eq!(result.current_address, "AA:BB:CC:DD:EE:02");
assert!(handler.lookup_by_address("AA:BB:CC:DD:EE:01").is_none());
assert_eq!(
handler.lookup_by_address("AA:BB:CC:DD:EE:02"),
Some(node_id)
);
assert_eq!(handler.lookup_by_name("WEAROS-1234"), Some(node_id));
}
#[test]
fn test_remove_device_cleans_all_maps() {
let mut handler = AddressRotationHandler::new();
let node_id = NodeId::new(0x12345678);
handler.register_device("WEAROS-ABCD", "AA:BB:CC:DD:EE:01", node_id);
assert_eq!(handler.lookup_by_name("WEAROS-ABCD"), Some(node_id));
assert_eq!(
handler.lookup_by_address("AA:BB:CC:DD:EE:01"),
Some(node_id)
);
assert_eq!(
handler.get_address(&node_id),
Some(&"AA:BB:CC:DD:EE:01".to_string())
);
assert_eq!(handler.get_name(&node_id), Some(&"WEAROS-ABCD".to_string()));
assert_eq!(handler.device_count(), 1);
handler.remove_device(&node_id);
assert!(handler.lookup_by_name("WEAROS-ABCD").is_none());
assert!(handler.lookup_by_address("AA:BB:CC:DD:EE:01").is_none());
assert!(handler.get_address(&node_id).is_none());
assert!(handler.get_name(&node_id).is_none());
assert_eq!(handler.device_count(), 0);
let stats = handler.stats();
assert_eq!(stats.devices_with_names, 0);
assert_eq!(stats.total_devices, 0);
assert_eq!(stats.address_mappings, 0);
}
}