1use 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
33impl DiscoveredDevice {
34 pub fn is_local(&self) -> bool {
37 self.source_network.is_none() && self.source_address.is_none()
38 }
39}
40
41#[derive(Debug, Clone)]
47pub struct RoutedDeviceConfig {
48 pub instance: u32,
50 pub router_mac: Vec<u8>,
52 pub remote_network: u16,
54 pub remote_mac: Vec<u8>,
56 pub max_apdu_length: u32,
58 pub segmentation_supported: Segmentation,
60 pub max_segments_accepted: Option<u32>,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub(crate) enum PeerSegmentation {
71 Authoritative(Segmentation),
74 Placeholder,
76}
77
78#[derive(Debug)]
79struct TableEntry {
80 device: DiscoveredDevice,
81 segmentation: PeerSegmentation,
82}
83
84#[derive(Debug, Default)]
109pub struct DeviceTable {
110 devices: HashMap<u32, TableEntry>,
111}
112
113#[derive(Debug, Clone)]
114pub(crate) enum DeviceUpsertResult {
115 Inserted,
116 Updated,
117 Collision { retained: DiscoveredDevice },
118 Dropped,
119}
120
121enum DeviceEndpoint<'a> {
122 Local(&'a [u8]),
123 Routed(u16, &'a [u8]),
124 Incomplete,
125}
126
127impl DeviceTable {
128 pub fn new() -> Self {
129 Self {
130 devices: HashMap::new(),
131 }
132 }
133
134 pub fn upsert(&mut self, device: DiscoveredDevice) {
144 let segmentation = PeerSegmentation::Authoritative(device.segmentation_supported);
145 let _ = self.insert_entry(device, segmentation);
146 }
147
148 pub(crate) fn upsert_with_result(&mut self, device: DiscoveredDevice) -> DeviceUpsertResult {
149 let key = device.object_identifier.instance_number();
150 if let Some(existing) = self.devices.get(&key) {
151 let replace = existing.segmentation == PeerSegmentation::Placeholder
152 || match (Self::endpoint(&existing.device), Self::endpoint(&device)) {
153 (DeviceEndpoint::Incomplete, _) => true,
156 (_, DeviceEndpoint::Incomplete) => false,
158 (DeviceEndpoint::Local(current), DeviceEndpoint::Local(incoming)) => {
159 current == incoming
160 }
161 (
162 DeviceEndpoint::Routed(current_network, current_address),
163 DeviceEndpoint::Routed(incoming_network, incoming_address),
164 ) => current_network == incoming_network && current_address == incoming_address,
165 _ => false,
166 };
167 if !replace {
168 return DeviceUpsertResult::Collision {
169 retained: existing.device.clone(),
170 };
171 }
172 }
173
174 let segmentation = PeerSegmentation::Authoritative(device.segmentation_supported);
175 self.insert_entry(device, segmentation)
176 }
177
178 fn endpoint(device: &DiscoveredDevice) -> DeviceEndpoint<'_> {
179 match (&device.source_network, &device.source_address) {
180 (None, None) => DeviceEndpoint::Local(device.mac_address.as_slice()),
181 (Some(network), Some(address)) => DeviceEndpoint::Routed(*network, address.as_slice()),
182 _ => DeviceEndpoint::Incomplete,
183 }
184 }
185
186 pub(crate) fn upsert_placeholder(&mut self, device: DiscoveredDevice) {
191 let _ = self.insert_entry(device, PeerSegmentation::Placeholder);
192 }
193
194 fn insert_entry(
195 &mut self,
196 device: DiscoveredDevice,
197 segmentation: PeerSegmentation,
198 ) -> DeviceUpsertResult {
199 const MAX_DEVICE_TABLE_ENTRIES: usize = 4096;
200 let key = device.object_identifier.instance_number();
201 let is_existing = self.devices.contains_key(&key);
202 if !is_existing && self.devices.len() >= MAX_DEVICE_TABLE_ENTRIES {
203 return DeviceUpsertResult::Dropped;
204 }
205 self.devices.insert(
206 key,
207 TableEntry {
208 device,
209 segmentation,
210 },
211 );
212 if is_existing {
213 DeviceUpsertResult::Updated
214 } else {
215 DeviceUpsertResult::Inserted
216 }
217 }
218
219 pub fn all(&self) -> Vec<DiscoveredDevice> {
221 self.devices.values().map(|e| e.device.clone()).collect()
222 }
223
224 pub fn get(&self, instance: u32) -> Option<&DiscoveredDevice> {
226 self.devices.get(&instance).map(|e| &e.device)
227 }
228
229 pub fn get_by_mac(&self, mac: &[u8]) -> Option<&DiscoveredDevice> {
240 self.devices
241 .values()
242 .filter(|e| e.device.is_local())
243 .filter(|e| e.device.mac_address.as_slice() == mac)
244 .max_by_key(|e| e.device.last_seen)
245 .map(|e| &e.device)
246 }
247
248 pub fn get_by_network_address(
260 &self,
261 network: u16,
262 address: &[u8],
263 ) -> Option<&DiscoveredDevice> {
264 self.devices
265 .values()
266 .filter(|e| {
267 e.device.source_network == Some(network)
268 && e.device
269 .source_address
270 .as_ref()
271 .is_some_and(|a| a.as_slice() == address)
272 })
273 .max_by_key(|e| e.device.last_seen)
274 .map(|e| &e.device)
275 }
276
277 pub(crate) fn local_peer_segmentation(&self, mac: &[u8]) -> Option<PeerSegmentation> {
281 let device = self.get_by_mac(mac)?;
282 self.devices
283 .get(&device.object_identifier.instance_number())
284 .map(|e| e.segmentation)
285 }
286
287 pub(crate) fn routed_peer_segmentation(
290 &self,
291 network: u16,
292 address: &[u8],
293 ) -> Option<PeerSegmentation> {
294 let device = self.get_by_network_address(network, address)?;
295 self.devices
296 .get(&device.object_identifier.instance_number())
297 .map(|e| e.segmentation)
298 }
299
300 pub fn clear(&mut self) {
302 self.devices.clear();
303 }
304
305 pub fn len(&self) -> usize {
307 self.devices.len()
308 }
309
310 pub fn is_empty(&self) -> bool {
312 self.devices.is_empty()
313 }
314
315 pub fn purge_stale(&mut self, max_age: Duration) {
317 let _ = self.purge_stale_at(Instant::now(), max_age);
318 }
319
320 pub(crate) fn purge_stale_collect(&mut self, max_age: Duration) -> Vec<DiscoveredDevice> {
321 self.purge_stale_at(Instant::now(), max_age)
322 }
323
324 fn purge_stale_at(&mut self, now: Instant, max_age: Duration) -> Vec<DiscoveredDevice> {
325 let stale_keys: Vec<u32> = self
326 .devices
327 .iter()
328 .filter_map(|(key, entry)| {
329 let is_stale = now
330 .checked_duration_since(entry.device.last_seen)
331 .is_some_and(|age| age > max_age);
332 is_stale.then_some(*key)
333 })
334 .collect();
335
336 stale_keys
337 .into_iter()
338 .filter_map(|key| self.devices.remove(&key).map(|e| e.device))
339 .collect()
340 }
341}
342
343#[cfg(test)]
344#[path = "discovery_collision_tests.rs"]
345mod discovery_collision_tests;
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use bacnet_types::enums::ObjectType;
351
352 fn make_device(instance: u32) -> DiscoveredDevice {
353 DiscoveredDevice {
354 object_identifier: ObjectIdentifier::new(ObjectType::DEVICE, instance).unwrap(),
355 mac_address: MacAddr::from_slice(&[192, 168, 1, 100, 0xBA, 0xC0]),
356 max_apdu_length: 1476,
357 segmentation_supported: Segmentation::NONE,
358 max_segments_accepted: None,
359 vendor_id: 42,
360 last_seen: Instant::now(),
361 source_network: None,
362 source_address: None,
363 }
364 }
365
366 #[test]
367 fn upsert_and_get() {
368 let mut table = DeviceTable::new();
369 table.upsert(make_device(1234));
370 assert_eq!(table.len(), 1);
371 let dev = table.get(1234).unwrap();
372 assert_eq!(dev.vendor_id, 42);
373 }
374
375 #[test]
376 fn upsert_updates_existing() {
377 let mut table = DeviceTable::new();
378 table.upsert(make_device(1234));
379 let mut updated = make_device(1234);
380 updated.vendor_id = 99;
381 table.upsert(updated);
382 assert_eq!(table.len(), 1);
383 assert_eq!(table.get(1234).unwrap().vendor_id, 99);
384 }
385
386 #[test]
387 fn upsert_with_result_reports_insert_and_update() {
388 let mut table = DeviceTable::new();
389 assert!(matches!(
390 table.upsert_with_result(make_device(1234)),
391 DeviceUpsertResult::Inserted
392 ));
393 assert!(matches!(
394 table.upsert_with_result(make_device(1234)),
395 DeviceUpsertResult::Updated
396 ));
397 }
398
399 #[test]
400 fn all_returns_snapshot() {
401 let mut table = DeviceTable::new();
402 table.upsert(make_device(1));
403 table.upsert(make_device(2));
404 table.upsert(make_device(3));
405 assert_eq!(table.all().len(), 3);
406 }
407
408 #[test]
409 fn clear_empties_table() {
410 let mut table = DeviceTable::new();
411 table.upsert(make_device(1));
412 table.clear();
413 assert!(table.is_empty());
414 }
415
416 #[test]
417 fn get_by_mac_finds_device() {
418 let mut table = DeviceTable::new();
419 table.upsert(make_device(1234));
420 let mac = &[192, 168, 1, 100, 0xBA, 0xC0];
421 let dev = table.get_by_mac(mac).unwrap();
422 assert_eq!(dev.object_identifier.instance_number(), 1234);
423 }
424
425 #[test]
426 fn get_by_mac_not_found() {
427 let mut table = DeviceTable::new();
428 table.upsert(make_device(1234));
429 assert!(table.get_by_mac(&[10, 0, 0, 1, 0xBA, 0xC0]).is_none());
430 }
431
432 fn make_routed_device(instance: u32, network: u16, address: &[u8]) -> DiscoveredDevice {
433 let mut device = make_device(instance);
434 device.source_network = Some(network);
435 device.source_address = Some(MacAddr::from_slice(address));
436 device
437 }
438
439 #[test]
440 fn get_by_network_address_finds_routed_device() {
441 let mut table = DeviceTable::new();
442 table.upsert(make_routed_device(1, 100, &[0x03]));
443 table.upsert(make_routed_device(2, 200, &[0x03]));
444 let dev = table.get_by_network_address(200, &[0x03]).unwrap();
445 assert_eq!(dev.object_identifier.instance_number(), 2);
446 }
447
448 #[test]
451 fn get_by_network_address_ignores_local_devices() {
452 let mut table = DeviceTable::new();
453 let mut local = make_device(1);
454 local.mac_address = MacAddr::from_slice(&[0x03]);
455 table.upsert(local);
456 assert!(table.get_by_network_address(100, &[0x03]).is_none());
457 }
458
459 #[test]
460 fn get_by_network_address_requires_both_terms() {
461 let mut table = DeviceTable::new();
462 table.upsert(make_routed_device(1, 100, &[0x03]));
463 assert!(table.get_by_network_address(100, &[0x04]).is_none());
464 assert!(table.get_by_network_address(101, &[0x03]).is_none());
465 }
466
467 #[test]
471 fn get_by_network_address_prefers_freshest_entry() {
472 let mut table = DeviceTable::new();
473 let now = Instant::now();
474 let mut stale = make_routed_device(1, 100, &[0x03]);
475 stale.max_apdu_length = 1476;
476 stale.last_seen = now;
477 let mut fresh = make_routed_device(2, 100, &[0x03]);
478 fresh.max_apdu_length = 128;
479 fresh.last_seen = now + Duration::from_secs(60);
480 table.upsert(stale);
481 table.upsert(fresh);
482
483 let dev = table.get_by_network_address(100, &[0x03]).unwrap();
484 assert_eq!(dev.object_identifier.instance_number(), 2);
485 assert_eq!(dev.max_apdu_length, 128);
486 }
487
488 #[test]
497 fn segmentation_provenance_tracks_row_lifecycle() {
498 let mut table = DeviceTable::new();
499 table.upsert_placeholder(make_device(1));
500 assert_eq!(
501 table.local_peer_segmentation(&[192, 168, 1, 100, 0xBA, 0xC0]),
502 Some(PeerSegmentation::Placeholder)
503 );
504
505 table.upsert_with_result(make_device(1));
507 assert_eq!(
508 table.local_peer_segmentation(&[192, 168, 1, 100, 0xBA, 0xC0]),
509 Some(PeerSegmentation::Authoritative(Segmentation::NONE))
510 );
511
512 let mut stale = make_device(1);
514 stale.last_seen = Instant::now() - Duration::from_secs(120);
515 table.upsert_placeholder(stale);
516 table.purge_stale(Duration::from_secs(60));
517 assert_eq!(
518 table.local_peer_segmentation(&[192, 168, 1, 100, 0xBA, 0xC0]),
519 None
520 );
521
522 table.upsert(make_device(2));
524 table.clear();
525 assert_eq!(
526 table.local_peer_segmentation(&[192, 168, 1, 100, 0xBA, 0xC0]),
527 None
528 );
529 }
530
531 #[test]
532 fn get_by_mac_ignores_routed_rows() {
533 let mut table = DeviceTable::new();
534 let router_mac = &[192, 168, 1, 1, 0xBA, 0xC0];
535 let routed = make_routed_device(3003, 100, &[0x03]);
536 let mut routed_with_router_mac = routed;
537 routed_with_router_mac.mac_address = MacAddr::from_slice(router_mac);
538 table.upsert(routed_with_router_mac);
539
540 assert!(table.get_by_mac(router_mac).is_none());
541 }
542
543 #[test]
546 fn get_by_mac_ignores_multiple_routed_rows_behind_one_router() {
547 let mut table = DeviceTable::new();
548 let router_mac = &[192, 168, 1, 1, 0xBA, 0xC0];
549 for instance in [1u32, 2, 3] {
550 let mut peer = make_routed_device(instance, 100, &[instance as u8]);
551 peer.mac_address = MacAddr::from_slice(router_mac);
552 table.upsert(peer);
553 }
554 assert!(table.get_by_mac(router_mac).is_none());
555 }
556
557 fn make_local_device_at_mac(instance: u32, mac: &[u8]) -> DiscoveredDevice {
558 let mut device = make_device(instance);
559 device.mac_address = MacAddr::from_slice(mac);
560 device
561 }
562
563 #[test]
566 fn get_by_mac_returns_local_row_not_routed_sadr_namesake() {
567 let mut table = DeviceTable::new();
568 table.upsert(make_local_device_at_mac(10, &[0x03]));
569 table.upsert(make_routed_device(3003, 100, &[0x03]));
570
571 let dev = table.get_by_mac(&[0x03]).unwrap();
572 assert_eq!(dev.object_identifier.instance_number(), 10);
573 }
574
575 #[test]
579 fn get_by_mac_prefers_freshest_local_row() {
580 let mut table = DeviceTable::new();
581 let now = Instant::now();
582 let mut stale = make_device(1);
583 stale.last_seen = now;
584 let mut fresh = make_device(2);
585 fresh.last_seen = now + Duration::from_secs(60);
586 table.upsert(stale);
587 table.upsert(fresh);
588
589 let dev = table.get_by_mac(&[192, 168, 1, 100, 0xBA, 0xC0]).unwrap();
590 assert_eq!(dev.object_identifier.instance_number(), 2);
591 }
592
593 #[test]
597 fn partial_routing_metadata_is_excluded_from_local_lookup() {
598 let mut table = DeviceTable::new();
599 let mut partial = make_device(7);
600 partial.source_network = Some(100);
601 table.upsert(partial);
602
603 assert!(table.get_by_mac(&[192, 168, 1, 100, 0xBA, 0xC0]).is_none());
604 assert!(table
605 .get_by_network_address(100, &[192, 168, 1, 100, 0xBA, 0xC0])
606 .is_none());
607 }
608
609 #[test]
610 fn purge_stale_removes_old_entries() {
611 let mut table = DeviceTable::new();
612 let now = Instant::now();
613 let mut old_device = make_device(1);
614 old_device.last_seen = now;
615 table.upsert(old_device);
616 let mut fresh_device = make_device(2);
617 fresh_device.last_seen = now + Duration::from_secs(120);
618 table.upsert(fresh_device);
619 assert_eq!(table.len(), 2);
620
621 let removed = table.purge_stale_at(now + Duration::from_secs(120), Duration::from_secs(60));
622 assert_eq!(table.len(), 1);
623 assert_eq!(removed.len(), 1);
624 assert_eq!(removed[0].object_identifier.instance_number(), 1);
625 assert!(table.get(1).is_none());
626 assert!(table.get(2).is_some());
627 }
628
629 #[test]
630 fn purge_stale_keeps_all_when_fresh() {
631 let mut table = DeviceTable::new();
632 table.upsert(make_device(1));
633 table.upsert(make_device(2));
634 table.purge_stale(Duration::from_secs(60));
635 assert_eq!(table.len(), 2);
636 }
637
638 #[test]
639 fn purge_stale_removes_all_when_expired() {
640 let mut table = DeviceTable::new();
641 let now = Instant::now();
642 let mut d1 = make_device(1);
643 d1.last_seen = now;
644 let mut d2 = make_device(2);
645 d2.last_seen = now;
646 table.upsert(d1);
647 table.upsert(d2);
648 let removed = table.purge_stale_at(now + Duration::from_secs(200), Duration::from_secs(60));
649 assert!(table.is_empty());
650 assert_eq!(removed.len(), 2);
651 }
652
653 #[test]
654 fn upsert_refreshes_last_seen() {
655 let mut table = DeviceTable::new();
656 let now = Instant::now();
657 let mut old_device = make_device(1);
658 old_device.last_seen = now;
659 table.upsert(old_device);
660
661 let mut refreshed = make_device(1);
662 refreshed.last_seen = now + Duration::from_secs(120);
663 table.upsert(refreshed);
664 let removed = table.purge_stale_at(now + Duration::from_secs(120), Duration::from_secs(60));
665 assert_eq!(table.len(), 1);
666 assert!(removed.is_empty());
667 assert!(table.get(1).is_some());
668 }
669
670 #[test]
671 fn purge_stale_handles_max_age_larger_than_instant_history() {
672 let mut table = DeviceTable::new();
673 table.upsert(make_device(1));
674
675 table.purge_stale(Duration::MAX);
676
677 assert_eq!(table.len(), 1);
678 }
679}