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