bacnet_client/
discovery.rs1use std::collections::HashMap;
4use std::time::{Duration, Instant};
5
6use bacnet_types::enums::Segmentation;
7use bacnet_types::primitives::ObjectIdentifier;
8use bacnet_types::MacAddr;
9
10#[derive(Debug, Clone)]
12pub struct DiscoveredDevice {
13 pub object_identifier: ObjectIdentifier,
15 pub mac_address: MacAddr,
17 pub max_apdu_length: u32,
19 pub segmentation_supported: Segmentation,
21 pub max_segments_accepted: Option<u32>,
23 pub vendor_id: u16,
25 pub last_seen: Instant,
27}
28
29#[derive(Debug, Default)]
34pub struct DeviceTable {
35 devices: HashMap<u32, DiscoveredDevice>,
36}
37
38impl DeviceTable {
39 pub fn new() -> Self {
40 Self {
41 devices: HashMap::new(),
42 }
43 }
44
45 pub fn upsert(&mut self, device: DiscoveredDevice) {
50 const MAX_DEVICE_TABLE_ENTRIES: usize = 4096;
51 let key = device.object_identifier.instance_number();
52 if !self.devices.contains_key(&key) && self.devices.len() >= MAX_DEVICE_TABLE_ENTRIES {
53 return; }
55 self.devices.insert(key, device);
56 }
57
58 pub fn all(&self) -> Vec<DiscoveredDevice> {
60 self.devices.values().cloned().collect()
61 }
62
63 pub fn get(&self, instance: u32) -> Option<&DiscoveredDevice> {
65 self.devices.get(&instance)
66 }
67
68 pub fn get_by_mac(&self, mac: &[u8]) -> Option<&DiscoveredDevice> {
70 self.devices
71 .values()
72 .find(|d| d.mac_address.as_slice() == mac)
73 }
74
75 pub fn clear(&mut self) {
77 self.devices.clear();
78 }
79
80 pub fn len(&self) -> usize {
82 self.devices.len()
83 }
84
85 pub fn is_empty(&self) -> bool {
87 self.devices.is_empty()
88 }
89
90 pub fn purge_stale(&mut self, max_age: Duration) {
92 let cutoff = Instant::now() - max_age;
93 self.devices.retain(|_, d| d.last_seen >= cutoff);
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use bacnet_types::enums::ObjectType;
101
102 fn make_device(instance: u32) -> DiscoveredDevice {
103 DiscoveredDevice {
104 object_identifier: ObjectIdentifier::new(ObjectType::DEVICE, instance).unwrap(),
105 mac_address: MacAddr::from_slice(&[192, 168, 1, 100, 0xBA, 0xC0]),
106 max_apdu_length: 1476,
107 segmentation_supported: Segmentation::NONE,
108 max_segments_accepted: None,
109 vendor_id: 42,
110 last_seen: Instant::now(),
111 }
112 }
113
114 #[test]
115 fn upsert_and_get() {
116 let mut table = DeviceTable::new();
117 table.upsert(make_device(1234));
118 assert_eq!(table.len(), 1);
119 let dev = table.get(1234).unwrap();
120 assert_eq!(dev.vendor_id, 42);
121 }
122
123 #[test]
124 fn upsert_updates_existing() {
125 let mut table = DeviceTable::new();
126 table.upsert(make_device(1234));
127 let mut updated = make_device(1234);
128 updated.vendor_id = 99;
129 table.upsert(updated);
130 assert_eq!(table.len(), 1);
131 assert_eq!(table.get(1234).unwrap().vendor_id, 99);
132 }
133
134 #[test]
135 fn all_returns_snapshot() {
136 let mut table = DeviceTable::new();
137 table.upsert(make_device(1));
138 table.upsert(make_device(2));
139 table.upsert(make_device(3));
140 assert_eq!(table.all().len(), 3);
141 }
142
143 #[test]
144 fn clear_empties_table() {
145 let mut table = DeviceTable::new();
146 table.upsert(make_device(1));
147 table.clear();
148 assert!(table.is_empty());
149 }
150
151 #[test]
152 fn get_by_mac_finds_device() {
153 let mut table = DeviceTable::new();
154 table.upsert(make_device(1234));
155 let mac = &[192, 168, 1, 100, 0xBA, 0xC0];
156 let dev = table.get_by_mac(mac).unwrap();
157 assert_eq!(dev.object_identifier.instance_number(), 1234);
158 }
159
160 #[test]
161 fn get_by_mac_not_found() {
162 let mut table = DeviceTable::new();
163 table.upsert(make_device(1234));
164 assert!(table.get_by_mac(&[10, 0, 0, 1, 0xBA, 0xC0]).is_none());
165 }
166
167 #[test]
168 fn purge_stale_removes_old_entries() {
169 let mut table = DeviceTable::new();
170 let mut old_device = make_device(1);
172 old_device.last_seen = Instant::now() - Duration::from_secs(120);
173 table.upsert(old_device);
174 table.upsert(make_device(2));
176 assert_eq!(table.len(), 2);
177
178 table.purge_stale(Duration::from_secs(60));
179 assert_eq!(table.len(), 1);
180 assert!(table.get(1).is_none());
181 assert!(table.get(2).is_some());
182 }
183
184 #[test]
185 fn purge_stale_keeps_all_when_fresh() {
186 let mut table = DeviceTable::new();
187 table.upsert(make_device(1));
188 table.upsert(make_device(2));
189 table.purge_stale(Duration::from_secs(60));
190 assert_eq!(table.len(), 2);
191 }
192
193 #[test]
194 fn purge_stale_removes_all_when_expired() {
195 let mut table = DeviceTable::new();
196 let mut d1 = make_device(1);
197 d1.last_seen = Instant::now() - Duration::from_secs(200);
198 let mut d2 = make_device(2);
199 d2.last_seen = Instant::now() - Duration::from_secs(200);
200 table.upsert(d1);
201 table.upsert(d2);
202 table.purge_stale(Duration::from_secs(60));
203 assert!(table.is_empty());
204 }
205
206 #[test]
207 fn upsert_refreshes_last_seen() {
208 let mut table = DeviceTable::new();
209 let mut old_device = make_device(1);
210 old_device.last_seen = Instant::now() - Duration::from_secs(120);
211 table.upsert(old_device);
212
213 table.upsert(make_device(1));
215
216 table.purge_stale(Duration::from_secs(60));
218 assert_eq!(table.len(), 1);
219 assert!(table.get(1).is_some());
220 }
221}