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