1use std::collections::HashMap;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::{Arc, Mutex};
9use std::time::{Duration, Instant};
10
11use bytes::BytesMut;
12
13use bacnet_encoding::apdu::{encode_apdu, Apdu, UnconfirmedRequest as UnconfirmedRequestPdu};
14use bacnet_encoding::npdu::NpduAddress;
15use bacnet_network::layer::{NetworkLayer, ReceivedApdu};
16use bacnet_objects::database::ObjectDatabase;
17use bacnet_services::who_has::{WhoHasObject, WhoHasRequest};
18use bacnet_services::who_is::{DeviceInstanceRange, IAmRequest, WhoIsRequest};
19use bacnet_transport::port::TransportPort;
20use bacnet_types::enums::{ErrorClass, ErrorCode, NetworkPriority, UnconfirmedServiceChoice};
21use bacnet_types::error::Error;
22use bacnet_types::primitives::ObjectIdentifier;
23use bacnet_types::MacAddr;
24use tokio::sync::RwLock;
25
26use super::{BACnetServer, CommState, ServerConfig};
27
28#[derive(Debug, Clone)]
30pub struct DiscoveryPolicy {
31 pub max_responses_per_sec_global: u32,
33 pub max_bytes_per_sec_global: usize,
35 pub max_responses_per_sec_per_source: u32,
37 pub max_bytes_per_sec_per_source: usize,
39 pub global_burst_capacity: u32,
41 pub source_burst_capacity: u32,
43 pub reserved_capacity: u32,
45 pub reserved_sources: Vec<MacAddr>,
47 pub coalesce_window: Duration,
49 pub prefer_directed_responses: bool,
51 pub max_tracked_sources: usize,
53}
54
55impl Default for DiscoveryPolicy {
56 fn default() -> Self {
57 Self {
58 max_responses_per_sec_global: 64,
59 max_bytes_per_sec_global: 65_536,
60 max_responses_per_sec_per_source: 16,
61 max_bytes_per_sec_per_source: 16_384,
62 global_burst_capacity: 64,
63 source_burst_capacity: 16,
64 reserved_capacity: 8,
65 reserved_sources: Vec::new(),
66 coalesce_window: Duration::from_millis(200),
67 prefer_directed_responses: true,
68 max_tracked_sources: 256,
69 }
70 }
71}
72
73impl DiscoveryPolicy {
74 pub fn unlimited() -> Self {
76 Self {
77 max_responses_per_sec_global: u32::MAX,
78 max_bytes_per_sec_global: usize::MAX,
79 max_responses_per_sec_per_source: u32::MAX,
80 max_bytes_per_sec_per_source: usize::MAX,
81 global_burst_capacity: u32::MAX,
82 source_burst_capacity: u32::MAX,
83 reserved_capacity: 0,
84 coalesce_window: Duration::ZERO,
85 prefer_directed_responses: false,
86 ..Default::default()
87 }
88 }
89
90 pub fn sanitized(&self) -> Self {
92 let mut policy = self.clone();
93 if (1..u32::MAX).contains(&policy.max_responses_per_sec_global) {
94 policy.global_burst_capacity = policy.global_burst_capacity.max(1);
95 }
96 if (1..u32::MAX).contains(&policy.max_responses_per_sec_per_source) {
97 policy.source_burst_capacity = policy.source_burst_capacity.max(1);
98 }
99 policy.reserved_capacity = policy.reserved_capacity.min(policy.global_burst_capacity);
100 policy.max_tracked_sources = policy.max_tracked_sources.max(1);
101 policy
102 }
103}
104
105#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
111#[non_exhaustive]
112pub struct DiscoveryCounters {
113 pub who_is_received: u64,
115 pub who_has_received: u64,
117 pub i_am_sent: u64,
119 pub i_have_sent: u64,
121 pub response_bytes_sent: u64,
123 pub requests_coalesced: u64,
125 pub responses_throttled_source: u64,
127 pub responses_throttled_global: u64,
129 pub directed_responses_sent: u64,
131 pub malformed_dropped: u64,
135}
136
137#[derive(Debug, Default)]
138pub(crate) struct AtomicDiscoveryCounters {
139 pub(crate) who_is_received: AtomicU64,
140 pub(crate) who_has_received: AtomicU64,
141 pub(crate) i_am_sent: AtomicU64,
142 pub(crate) i_have_sent: AtomicU64,
143 pub(crate) response_bytes_sent: AtomicU64,
144 pub(crate) requests_coalesced: AtomicU64,
145 pub(crate) responses_throttled_source: AtomicU64,
146 pub(crate) responses_throttled_global: AtomicU64,
147 pub(crate) directed_responses_sent: AtomicU64,
148 pub(crate) malformed_dropped: AtomicU64,
149}
150
151impl AtomicDiscoveryCounters {
152 pub(crate) fn snapshot(&self) -> DiscoveryCounters {
153 DiscoveryCounters {
154 who_is_received: self.who_is_received.load(Ordering::Relaxed),
155 who_has_received: self.who_has_received.load(Ordering::Relaxed),
156 i_am_sent: self.i_am_sent.load(Ordering::Relaxed),
157 i_have_sent: self.i_have_sent.load(Ordering::Relaxed),
158 response_bytes_sent: self.response_bytes_sent.load(Ordering::Relaxed),
159 requests_coalesced: self.requests_coalesced.load(Ordering::Relaxed),
160 responses_throttled_source: self.responses_throttled_source.load(Ordering::Relaxed),
161 responses_throttled_global: self.responses_throttled_global.load(Ordering::Relaxed),
162 directed_responses_sent: self.directed_responses_sent.load(Ordering::Relaxed),
163 malformed_dropped: self.malformed_dropped.load(Ordering::Relaxed),
164 }
165 }
166
167 #[inline]
168 pub(crate) fn inc(&self, c: &AtomicU64) {
169 c.fetch_add(1, Ordering::Relaxed);
170 }
171
172 pub(crate) fn record_sent(&self, bytes: usize, directed: bool, is_who_is: bool) {
173 if is_who_is {
174 self.i_am_sent.fetch_add(1, Ordering::Relaxed);
175 } else {
176 self.i_have_sent.fetch_add(1, Ordering::Relaxed);
177 }
178 self.response_bytes_sent
179 .fetch_add(bytes as u64, Ordering::Relaxed);
180 if directed {
181 self.directed_responses_sent.fetch_add(1, Ordering::Relaxed);
182 }
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Hash)]
188pub(crate) enum WhoHasTarget {
189 Id(ObjectIdentifier),
190 Name(String),
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Hash)]
195pub(crate) struct SourceKey {
196 pub(crate) network: u16,
197 pub(crate) mac: MacAddr,
198}
199
200impl SourceKey {
201 pub(crate) fn from_parts(source_mac: &MacAddr, source_network: Option<&NpduAddress>) -> Self {
202 if let Some(net) = source_network {
203 if (1..=0xFFFE).contains(&net.network) && !net.mac_address.is_empty() {
204 return Self {
205 network: net.network,
206 mac: MacAddr::from_slice(&net.mac_address),
207 };
208 }
209 }
210 Self {
211 network: 0,
212 mac: source_mac.clone(),
213 }
214 }
215
216 pub(crate) fn from_received(received: &ReceivedApdu) -> Self {
217 Self::from_parts(&received.source_mac, received.source_network.as_ref())
218 }
219}
220
221#[derive(Debug)]
222struct SourceState {
223 tokens: f64,
224 byte_tokens: f64,
225 last_refill: Instant,
226 last_activity: Instant,
227 last_who_is: Option<Instant>,
228 last_who_has: HashMap<WhoHasTarget, Instant>,
229}
230
231fn refill_bucket(
232 tokens: &mut f64,
233 byte_tokens: &mut f64,
234 last_refill: &mut Instant,
235 rate: u32,
236 byte_rate: usize,
237 burst: u32,
238 now: Instant,
239) {
240 let elapsed = now.saturating_duration_since(*last_refill).as_secs_f64();
241 if elapsed > 0.0 {
242 *tokens = if rate == u32::MAX {
243 burst as f64
244 } else {
245 (*tokens + elapsed * (rate as f64)).min(burst as f64)
246 };
247 *byte_tokens = if byte_rate == usize::MAX {
248 byte_rate as f64
249 } else {
250 (*byte_tokens + elapsed * (byte_rate as f64)).min(byte_rate as f64)
251 };
252 *last_refill = now;
253 }
254}
255
256impl SourceState {
257 fn refill(&mut self, policy: &DiscoveryPolicy, now: Instant) {
258 refill_bucket(
259 &mut self.tokens,
260 &mut self.byte_tokens,
261 &mut self.last_refill,
262 policy.max_responses_per_sec_per_source,
263 policy.max_bytes_per_sec_per_source,
264 policy.source_burst_capacity,
265 now,
266 );
267 }
268
269 fn has_capacity(&self, policy: &DiscoveryPolicy, bytes: usize) -> bool {
270 (policy.max_responses_per_sec_per_source == u32::MAX || self.tokens >= 1.0)
271 && (policy.max_bytes_per_sec_per_source == usize::MAX
272 || self.byte_tokens >= bytes as f64)
273 }
274
275 fn deduct(&mut self, policy: &DiscoveryPolicy, bytes: usize) {
276 if policy.max_responses_per_sec_per_source != u32::MAX {
277 self.tokens -= 1.0;
278 }
279 if policy.max_bytes_per_sec_per_source != usize::MAX {
280 self.byte_tokens -= bytes as f64;
281 }
282 }
283}
284
285#[derive(Debug)]
286struct DiscoveryState {
287 global_tokens: f64,
288 global_byte_tokens: f64,
289 global_last_refill: Instant,
290 last_broadcast_who_is: Option<Instant>,
291 last_broadcast_who_has: HashMap<WhoHasTarget, Instant>,
292 negative_who_has: HashMap<WhoHasTarget, Instant>,
293 sources: HashMap<SourceKey, SourceState>,
294}
295
296impl DiscoveryState {
297 fn refill_global(&mut self, policy: &DiscoveryPolicy, now: Instant) {
298 refill_bucket(
299 &mut self.global_tokens,
300 &mut self.global_byte_tokens,
301 &mut self.global_last_refill,
302 policy.max_responses_per_sec_global,
303 policy.max_bytes_per_sec_global,
304 policy.global_burst_capacity,
305 now,
306 );
307 }
308
309 fn has_global_capacity(
310 &self,
311 policy: &DiscoveryPolicy,
312 is_reserved: bool,
313 bytes: usize,
314 ) -> bool {
315 let count_ok = policy.max_responses_per_sec_global == u32::MAX || {
316 let req = if is_reserved {
317 1.0
318 } else {
319 1.0 + policy.reserved_capacity as f64
320 };
321 self.global_tokens >= req
322 };
323 count_ok
324 && (policy.max_bytes_per_sec_global == usize::MAX
325 || self.global_byte_tokens >= bytes as f64)
326 }
327
328 fn deduct_global(&mut self, policy: &DiscoveryPolicy, bytes: usize) {
329 if policy.max_responses_per_sec_global != u32::MAX {
330 self.global_tokens -= 1.0;
331 }
332 if policy.max_bytes_per_sec_global != usize::MAX {
333 self.global_byte_tokens -= bytes as f64;
334 }
335 }
336
337 fn get_or_create_source_mut(
338 &mut self,
339 policy: &DiscoveryPolicy,
340 key: &SourceKey,
341 now: Instant,
342 ) -> &mut SourceState {
343 if !self.sources.contains_key(key) {
344 ensure_source_capacity(self, policy, now);
345 }
346 let src = self
347 .sources
348 .entry(key.clone())
349 .or_insert_with(|| SourceState {
350 tokens: policy.source_burst_capacity as f64,
351 byte_tokens: policy.max_bytes_per_sec_per_source as f64,
352 last_refill: now,
353 last_activity: now,
354 last_who_is: None,
355 last_who_has: HashMap::new(),
356 });
357 src.refill(policy, now);
358 src
359 }
360
361 fn is_coalesced_who_has(
362 &self,
363 policy: &DiscoveryPolicy,
364 target: &WhoHasTarget,
365 key: &SourceKey,
366 is_broadcast: bool,
367 now: Instant,
368 ) -> bool {
369 if policy.coalesce_window.is_zero() {
370 return false;
371 }
372 let is_recent = |ts: &Instant| now.saturating_duration_since(*ts) < policy.coalesce_window;
373 self.negative_who_has.get(target).is_some_and(is_recent)
374 || (is_broadcast
375 && self
376 .last_broadcast_who_has
377 .get(target)
378 .is_some_and(is_recent))
379 || self
380 .sources
381 .get(key)
382 .and_then(|s| s.last_who_has.get(target))
383 .is_some_and(is_recent)
384 }
385}
386
387#[derive(Debug)]
388struct AtomicDeviceInstance(AtomicU64);
389
390impl AtomicDeviceInstance {
391 const PRESENT_BIT: u64 = 0x1_0000_0000;
392
393 fn new(opt: Option<u32>) -> Self {
394 Self(AtomicU64::new(
395 opt.map_or(0, |i| Self::PRESENT_BIT | (i as u64)),
396 ))
397 }
398
399 fn load(&self, order: Ordering) -> Option<u32> {
400 let val = self.0.load(order);
401 (val & Self::PRESENT_BIT != 0).then_some(val as u32)
402 }
403}
404
405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
406pub(crate) enum PreCheckDecision {
407 Admit,
408 OutOfRange,
409 Coalesced,
410 ThrottledSource,
411 ThrottledGlobal,
412 DecodeError,
413}
414
415#[derive(Debug)]
417pub(crate) struct DiscoveryLimiter {
418 policy: DiscoveryPolicy,
419 counters: AtomicDiscoveryCounters,
420 device_instance: AtomicDeviceInstance,
421 state: Mutex<DiscoveryState>,
422}
423
424impl DiscoveryLimiter {
425 pub(crate) fn new(policy: DiscoveryPolicy, device_instance: Option<u32>) -> Self {
429 let now = super::runtime_clock::now();
430 let state = DiscoveryState {
431 global_tokens: policy.global_burst_capacity as f64,
432 global_byte_tokens: policy.max_bytes_per_sec_global as f64,
433 global_last_refill: now,
434 last_broadcast_who_is: None,
435 last_broadcast_who_has: HashMap::new(),
436 negative_who_has: HashMap::new(),
437 sources: HashMap::new(),
438 };
439 Self {
440 policy,
441 counters: AtomicDiscoveryCounters::default(),
442 device_instance: AtomicDeviceInstance::new(device_instance),
443 state: Mutex::new(state),
444 }
445 }
446
447 pub(crate) fn counters(&self) -> DiscoveryCounters {
448 self.counters.snapshot()
449 }
450
451 #[allow(dead_code)]
452 pub(crate) fn policy(&self) -> &DiscoveryPolicy {
453 &self.policy
454 }
455
456 #[cfg(test)]
457 pub(crate) fn negative_who_has_count(&self) -> usize {
458 self.state.lock().unwrap().negative_who_has.len()
459 }
460
461 #[cfg(test)]
462 pub(crate) fn source_byte_tokens(
463 &self,
464 source_mac: &MacAddr,
465 source_network: Option<&NpduAddress>,
466 ) -> Option<f64> {
467 let key = SourceKey::from_parts(source_mac, source_network);
468 self.state
469 .lock()
470 .unwrap()
471 .sources
472 .get(&key)
473 .map(|s| s.byte_tokens)
474 }
475
476 fn check_instance_in_range(&self, range: Option<DeviceInstanceRange>) -> bool {
477 self.device_instance
478 .load(Ordering::Acquire)
479 .is_some_and(|instance| range.is_none_or(|range| range.contains(instance)))
480 }
481
482 pub(crate) fn pre_check_who_is(
483 &self,
484 req_bytes: &[u8],
485 received: &ReceivedApdu,
486 now: Instant,
487 ) -> PreCheckDecision {
488 let _ = received.provenance;
490 let who_is = match WhoIsRequest::decode(req_bytes) {
491 Ok(w) => w,
492 Err(_) => {
493 self.counters.inc(&self.counters.malformed_dropped);
494 return PreCheckDecision::DecodeError;
495 }
496 };
497
498 self.counters.inc(&self.counters.who_is_received);
499
500 if !self.check_instance_in_range(who_is.range) {
501 return PreCheckDecision::OutOfRange;
502 }
503
504 let is_broadcast = received.is_group || received.link_layer_group;
505 let source_key = SourceKey::from_received(received);
506 let mut state = self.state.lock().unwrap();
507
508 if !self.policy.coalesce_window.is_zero() {
510 let is_recent =
511 |ts: &Instant| now.saturating_duration_since(*ts) < self.policy.coalesce_window;
512 let bcast = is_broadcast && state.last_broadcast_who_is.as_ref().is_some_and(is_recent);
513 let src = state
514 .sources
515 .get(&source_key)
516 .and_then(|s| s.last_who_is.as_ref())
517 .is_some_and(is_recent);
518 if bcast || src {
519 self.counters.inc(&self.counters.requests_coalesced);
520 return PreCheckDecision::Coalesced;
521 }
522 }
523
524 state.refill_global(&self.policy, now);
526 let is_reserved = is_source_reserved(&self.policy, &source_key, received);
527 const ESTIMATED_I_AM_BYTES: usize = 64;
528
529 if !state.has_global_capacity(&self.policy, is_reserved, ESTIMATED_I_AM_BYTES) {
530 self.counters.inc(&self.counters.responses_throttled_global);
531 return PreCheckDecision::ThrottledGlobal;
532 }
533
534 let src = state.get_or_create_source_mut(&self.policy, &source_key, now);
535 if !src.has_capacity(&self.policy, ESTIMATED_I_AM_BYTES) {
536 self.counters.inc(&self.counters.responses_throttled_source);
537 return PreCheckDecision::ThrottledSource;
538 }
539
540 src.deduct(&self.policy, ESTIMATED_I_AM_BYTES);
541 src.last_activity = now;
542 src.last_who_is = Some(now);
543 state.deduct_global(&self.policy, ESTIMATED_I_AM_BYTES);
544 if is_broadcast && !self.policy.prefer_directed_responses {
545 state.last_broadcast_who_is = Some(now);
546 }
547
548 PreCheckDecision::Admit
549 }
550
551 pub(crate) fn record_i_am_sent(
552 &self,
553 actual_bytes: usize,
554 directed: bool,
555 source_mac: &MacAddr,
556 source_network: Option<&NpduAddress>,
557 now: Instant,
558 ) {
559 self.counters.record_sent(actual_bytes, directed, true);
560
561 const ESTIMATED_I_AM_BYTES: usize = 64;
562 let delta = (actual_bytes as f64) - (ESTIMATED_I_AM_BYTES as f64);
563 let mut state = self.state.lock().unwrap();
564 if delta != 0.0 {
565 if self.policy.max_bytes_per_sec_global != usize::MAX {
566 state.global_byte_tokens = (state.global_byte_tokens - delta).max(0.0);
567 }
568 let key = SourceKey::from_parts(source_mac, source_network);
569 if let Some(src) = state.sources.get_mut(&key) {
570 if self.policy.max_bytes_per_sec_per_source != usize::MAX {
571 src.byte_tokens = (src.byte_tokens - delta).max(0.0);
572 }
573 }
574 }
575 if !directed {
576 state.last_broadcast_who_is = Some(now);
577 }
578 }
579
580 pub(crate) fn pre_check_who_has(
581 &self,
582 req_bytes: &[u8],
583 received: &ReceivedApdu,
584 now: Instant,
585 ) -> PreCheckDecision {
586 let _ = received.provenance;
588 let who_has = match WhoHasRequest::decode(req_bytes) {
589 Ok(w) => w,
590 Err(_) => {
591 self.counters.inc(&self.counters.malformed_dropped);
592 return PreCheckDecision::DecodeError;
593 }
594 };
595
596 self.counters.inc(&self.counters.who_has_received);
597
598 if !self.check_instance_in_range(who_has.range) {
599 return PreCheckDecision::OutOfRange;
600 }
601
602 let target = match who_has.object {
603 WhoHasObject::Identifier(oid) => WhoHasTarget::Id(oid),
604 WhoHasObject::Name(name) => WhoHasTarget::Name(name),
605 };
606
607 let is_broadcast = received.is_group || received.link_layer_group;
608 let source_key = SourceKey::from_received(received);
609 let mut state = self.state.lock().unwrap();
610
611 if state.is_coalesced_who_has(&self.policy, &target, &source_key, is_broadcast, now) {
612 self.counters.inc(&self.counters.requests_coalesced);
613 return PreCheckDecision::Coalesced;
614 }
615
616 state.refill_global(&self.policy, now);
617 let is_reserved = is_source_reserved(&self.policy, &source_key, received);
618 const ESTIMATED_I_HAVE_BYTES: usize = 64;
619
620 if !state.has_global_capacity(&self.policy, is_reserved, ESTIMATED_I_HAVE_BYTES) {
621 self.counters.inc(&self.counters.responses_throttled_global);
622 return PreCheckDecision::ThrottledGlobal;
623 }
624
625 let src = state.get_or_create_source_mut(&self.policy, &source_key, now);
626 if !src.has_capacity(&self.policy, ESTIMATED_I_HAVE_BYTES) {
627 self.counters.inc(&self.counters.responses_throttled_source);
628 return PreCheckDecision::ThrottledSource;
629 }
630
631 src.last_activity = now;
632 PreCheckDecision::Admit
633 }
634
635 pub(crate) fn is_negative_who_has(&self, target: &WhoHasTarget, now: Instant) -> bool {
636 if self.policy.coalesce_window.is_zero() {
637 return false;
638 }
639 let state = self.state.lock().unwrap();
640 if let Some(ts) = state.negative_who_has.get(target) {
641 now.saturating_duration_since(*ts) < self.policy.coalesce_window
642 } else {
643 false
644 }
645 }
646
647 pub(crate) fn record_negative_who_has(&self, target: WhoHasTarget, now: Instant) {
648 const MAX_NEGATIVE_WHO_HAS: usize = 512;
649 let mut state = self.state.lock().unwrap();
650 if state.negative_who_has.len() >= MAX_NEGATIVE_WHO_HAS {
651 let window = self.policy.coalesce_window;
652 state
653 .negative_who_has
654 .retain(|_, ts| now.saturating_duration_since(*ts) < window);
655 }
656 while state.negative_who_has.len() >= MAX_NEGATIVE_WHO_HAS {
657 let Some(oldest) = state
658 .negative_who_has
659 .iter()
660 .min_by_key(|(_, ts)| *ts)
661 .map(|(k, _)| k.clone())
662 else {
663 break;
664 };
665 state.negative_who_has.remove(&oldest);
666 }
667 state.negative_who_has.insert(target, now);
668 }
669
670 pub(crate) fn try_consume_who_has_response(
671 &self,
672 target: &WhoHasTarget,
673 received: &ReceivedApdu,
674 response_bytes: usize,
675 now: Instant,
676 ) -> bool {
677 let source_key = SourceKey::from_received(received);
678 let mut state = self.state.lock().unwrap();
679 let is_broadcast = received.is_group || received.link_layer_group;
680
681 if state.is_coalesced_who_has(&self.policy, target, &source_key, is_broadcast, now) {
682 self.counters.inc(&self.counters.requests_coalesced);
683 return false;
684 }
685
686 state.refill_global(&self.policy, now);
687 let is_reserved = is_source_reserved(&self.policy, &source_key, received);
688
689 if !state.has_global_capacity(&self.policy, is_reserved, response_bytes) {
690 self.counters.inc(&self.counters.responses_throttled_global);
691 return false;
692 }
693
694 let src = state.get_or_create_source_mut(&self.policy, &source_key, now);
695 if !src.has_capacity(&self.policy, response_bytes) {
696 self.counters.inc(&self.counters.responses_throttled_source);
697 return false;
698 }
699
700 src.deduct(&self.policy, response_bytes);
701 src.last_activity = now;
702 src.last_who_has.insert(target.clone(), now);
703 state.deduct_global(&self.policy, response_bytes);
704 if is_broadcast && !self.policy.prefer_directed_responses {
705 state.last_broadcast_who_has.insert(target.clone(), now);
706 }
707
708 true
709 }
710
711 pub(crate) fn record_i_have_sent(
712 &self,
713 actual_bytes: usize,
714 directed: bool,
715 target: &WhoHasTarget,
716 _source_mac: &MacAddr,
717 _source_network: Option<&NpduAddress>,
718 now: Instant,
719 ) {
720 self.counters.record_sent(actual_bytes, directed, false);
721
722 if !directed {
723 let mut state = self.state.lock().unwrap();
724 state.last_broadcast_who_has.insert(target.clone(), now);
725 }
726 }
727}
728
729fn is_source_reserved(policy: &DiscoveryPolicy, key: &SourceKey, recv: &ReceivedApdu) -> bool {
730 policy
731 .reserved_sources
732 .iter()
733 .any(|r| r == &key.mac || r == &recv.source_mac)
734}
735
736fn ensure_source_capacity(state: &mut DiscoveryState, policy: &DiscoveryPolicy, now: Instant) {
737 if state.sources.len() < policy.max_tracked_sources {
738 return;
739 }
740 state.sources.retain(|_, src| {
741 now.saturating_duration_since(src.last_activity) < Duration::from_secs(10)
742 });
743 while state.sources.len() >= policy.max_tracked_sources {
744 let Some(key) = state
745 .sources
746 .iter()
747 .min_by_key(|(_, src)| src.last_activity)
748 .map(|(k, _)| k.clone())
749 else {
750 break;
751 };
752 state.sources.remove(&key);
753 }
754}
755
756pub(crate) async fn broadcast_i_am_from<T: TransportPort + 'static>(
762 config: &ServerConfig,
763 db: &Arc<RwLock<ObjectDatabase>>,
764 network: &Arc<NetworkLayer<T>>,
765 comm_state: &CommState,
766 limiter: Option<&Arc<DiscoveryLimiter>>,
767) -> Result<(), Error> {
768 let guard = db.read().await;
769 let device_oid =
770 crate::local_device::validate_apdu_declaration(&guard, config.max_apdu_length)?
771 .ok_or_else(|| Error::Encoding("no Device object in database".into()))?;
772
773 let mut service_buf = BytesMut::new();
779 iam_request_for(device_oid, config).encode(&mut service_buf);
780
781 let mut buf = BytesMut::new();
782 encode_apdu(
783 &mut buf,
784 &Apdu::UnconfirmedRequest(UnconfirmedRequestPdu {
785 service_choice: UnconfirmedServiceChoice::I_AM,
786 service_request: service_buf.freeze(),
787 }),
788 )?;
789
790 drop(guard);
791 if comm_state.initiation_restricted() {
792 return Err(Error::Protocol {
793 class: ErrorClass::SERVICES.to_raw() as u32,
794 code: ErrorCode::COMMUNICATION_DISABLED.to_raw() as u32,
795 });
796 }
797 if let Some(limiter) = limiter {
798 limiter.record_i_am_sent(
799 buf.len(),
800 false,
801 &MacAddr::new(),
802 None,
803 super::runtime_clock::now(),
804 );
805 }
806
807 network
808 .broadcast_apdu(&buf, false, NetworkPriority::NORMAL)
809 .await
810}
811
812#[doc(hidden)]
819pub fn iam_request_for(device_oid: ObjectIdentifier, config: &ServerConfig) -> IAmRequest {
820 IAmRequest {
821 object_identifier: device_oid,
822 max_apdu_length: config.max_apdu_length,
823 segmentation_supported: config.segmentation_supported,
824 vendor_id: config.vendor_id,
825 }
826}
827
828impl<T: TransportPort + 'static> BACnetServer<T> {
829 pub async fn broadcast_i_am(&self) -> Result<(), Error> {
834 self.broadcaster
835 .send(Some(Arc::clone(&self.discovery_limiter)))
836 .await
837 }
838}