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 {
426 let now = Instant::now();
427 let state = DiscoveryState {
428 global_tokens: policy.global_burst_capacity as f64,
429 global_byte_tokens: policy.max_bytes_per_sec_global as f64,
430 global_last_refill: now,
431 last_broadcast_who_is: None,
432 last_broadcast_who_has: HashMap::new(),
433 negative_who_has: HashMap::new(),
434 sources: HashMap::new(),
435 };
436 Self {
437 policy,
438 counters: AtomicDiscoveryCounters::default(),
439 device_instance: AtomicDeviceInstance::new(device_instance),
440 state: Mutex::new(state),
441 }
442 }
443
444 pub(crate) fn counters(&self) -> DiscoveryCounters {
445 self.counters.snapshot()
446 }
447
448 #[allow(dead_code)]
449 pub(crate) fn policy(&self) -> &DiscoveryPolicy {
450 &self.policy
451 }
452
453 #[cfg(test)]
454 pub(crate) fn negative_who_has_count(&self) -> usize {
455 self.state.lock().unwrap().negative_who_has.len()
456 }
457
458 #[cfg(test)]
459 pub(crate) fn source_byte_tokens(
460 &self,
461 source_mac: &MacAddr,
462 source_network: Option<&NpduAddress>,
463 ) -> Option<f64> {
464 let key = SourceKey::from_parts(source_mac, source_network);
465 self.state
466 .lock()
467 .unwrap()
468 .sources
469 .get(&key)
470 .map(|s| s.byte_tokens)
471 }
472
473 fn check_instance_in_range(&self, range: Option<DeviceInstanceRange>) -> bool {
474 self.device_instance
475 .load(Ordering::Acquire)
476 .is_some_and(|instance| range.is_none_or(|range| range.contains(instance)))
477 }
478
479 pub(crate) fn pre_check_who_is(
480 &self,
481 req_bytes: &[u8],
482 received: &ReceivedApdu,
483 now: Instant,
484 ) -> PreCheckDecision {
485 let _ = received.provenance;
487 let who_is = match WhoIsRequest::decode(req_bytes) {
488 Ok(w) => w,
489 Err(_) => {
490 self.counters.inc(&self.counters.malformed_dropped);
491 return PreCheckDecision::DecodeError;
492 }
493 };
494
495 self.counters.inc(&self.counters.who_is_received);
496
497 if !self.check_instance_in_range(who_is.range) {
498 return PreCheckDecision::OutOfRange;
499 }
500
501 let is_broadcast = received.is_group || received.link_layer_group;
502 let source_key = SourceKey::from_received(received);
503 let mut state = self.state.lock().unwrap();
504
505 if !self.policy.coalesce_window.is_zero() {
507 let is_recent =
508 |ts: &Instant| now.saturating_duration_since(*ts) < self.policy.coalesce_window;
509 let bcast = is_broadcast && state.last_broadcast_who_is.as_ref().is_some_and(is_recent);
510 let src = state
511 .sources
512 .get(&source_key)
513 .and_then(|s| s.last_who_is.as_ref())
514 .is_some_and(is_recent);
515 if bcast || src {
516 self.counters.inc(&self.counters.requests_coalesced);
517 return PreCheckDecision::Coalesced;
518 }
519 }
520
521 state.refill_global(&self.policy, now);
523 let is_reserved = is_source_reserved(&self.policy, &source_key, received);
524 const ESTIMATED_I_AM_BYTES: usize = 64;
525
526 if !state.has_global_capacity(&self.policy, is_reserved, ESTIMATED_I_AM_BYTES) {
527 self.counters.inc(&self.counters.responses_throttled_global);
528 return PreCheckDecision::ThrottledGlobal;
529 }
530
531 let src = state.get_or_create_source_mut(&self.policy, &source_key, now);
532 if !src.has_capacity(&self.policy, ESTIMATED_I_AM_BYTES) {
533 self.counters.inc(&self.counters.responses_throttled_source);
534 return PreCheckDecision::ThrottledSource;
535 }
536
537 src.deduct(&self.policy, ESTIMATED_I_AM_BYTES);
538 src.last_activity = now;
539 src.last_who_is = Some(now);
540 state.deduct_global(&self.policy, ESTIMATED_I_AM_BYTES);
541 if is_broadcast && !self.policy.prefer_directed_responses {
542 state.last_broadcast_who_is = Some(now);
543 }
544
545 PreCheckDecision::Admit
546 }
547
548 pub(crate) fn record_i_am_sent(
549 &self,
550 actual_bytes: usize,
551 directed: bool,
552 source_mac: &MacAddr,
553 source_network: Option<&NpduAddress>,
554 now: Instant,
555 ) {
556 self.counters.record_sent(actual_bytes, directed, true);
557
558 const ESTIMATED_I_AM_BYTES: usize = 64;
559 let delta = (actual_bytes as f64) - (ESTIMATED_I_AM_BYTES as f64);
560 let mut state = self.state.lock().unwrap();
561 if delta != 0.0 {
562 if self.policy.max_bytes_per_sec_global != usize::MAX {
563 state.global_byte_tokens = (state.global_byte_tokens - delta).max(0.0);
564 }
565 let key = SourceKey::from_parts(source_mac, source_network);
566 if let Some(src) = state.sources.get_mut(&key) {
567 if self.policy.max_bytes_per_sec_per_source != usize::MAX {
568 src.byte_tokens = (src.byte_tokens - delta).max(0.0);
569 }
570 }
571 }
572 if !directed {
573 state.last_broadcast_who_is = Some(now);
574 }
575 }
576
577 pub(crate) fn pre_check_who_has(
578 &self,
579 req_bytes: &[u8],
580 received: &ReceivedApdu,
581 now: Instant,
582 ) -> PreCheckDecision {
583 let _ = received.provenance;
585 let who_has = match WhoHasRequest::decode(req_bytes) {
586 Ok(w) => w,
587 Err(_) => {
588 self.counters.inc(&self.counters.malformed_dropped);
589 return PreCheckDecision::DecodeError;
590 }
591 };
592
593 self.counters.inc(&self.counters.who_has_received);
594
595 if !self.check_instance_in_range(who_has.range) {
596 return PreCheckDecision::OutOfRange;
597 }
598
599 let target = match who_has.object {
600 WhoHasObject::Identifier(oid) => WhoHasTarget::Id(oid),
601 WhoHasObject::Name(name) => WhoHasTarget::Name(name),
602 };
603
604 let is_broadcast = received.is_group || received.link_layer_group;
605 let source_key = SourceKey::from_received(received);
606 let mut state = self.state.lock().unwrap();
607
608 if state.is_coalesced_who_has(&self.policy, &target, &source_key, is_broadcast, now) {
609 self.counters.inc(&self.counters.requests_coalesced);
610 return PreCheckDecision::Coalesced;
611 }
612
613 state.refill_global(&self.policy, now);
614 let is_reserved = is_source_reserved(&self.policy, &source_key, received);
615 const ESTIMATED_I_HAVE_BYTES: usize = 64;
616
617 if !state.has_global_capacity(&self.policy, is_reserved, ESTIMATED_I_HAVE_BYTES) {
618 self.counters.inc(&self.counters.responses_throttled_global);
619 return PreCheckDecision::ThrottledGlobal;
620 }
621
622 let src = state.get_or_create_source_mut(&self.policy, &source_key, now);
623 if !src.has_capacity(&self.policy, ESTIMATED_I_HAVE_BYTES) {
624 self.counters.inc(&self.counters.responses_throttled_source);
625 return PreCheckDecision::ThrottledSource;
626 }
627
628 src.last_activity = now;
629 PreCheckDecision::Admit
630 }
631
632 pub(crate) fn is_negative_who_has(&self, target: &WhoHasTarget, now: Instant) -> bool {
633 if self.policy.coalesce_window.is_zero() {
634 return false;
635 }
636 let state = self.state.lock().unwrap();
637 if let Some(ts) = state.negative_who_has.get(target) {
638 now.saturating_duration_since(*ts) < self.policy.coalesce_window
639 } else {
640 false
641 }
642 }
643
644 pub(crate) fn record_negative_who_has(&self, target: WhoHasTarget, now: Instant) {
645 const MAX_NEGATIVE_WHO_HAS: usize = 512;
646 let mut state = self.state.lock().unwrap();
647 if state.negative_who_has.len() >= MAX_NEGATIVE_WHO_HAS {
648 let window = self.policy.coalesce_window;
649 state
650 .negative_who_has
651 .retain(|_, ts| now.saturating_duration_since(*ts) < window);
652 }
653 while state.negative_who_has.len() >= MAX_NEGATIVE_WHO_HAS {
654 let Some(oldest) = state
655 .negative_who_has
656 .iter()
657 .min_by_key(|(_, ts)| *ts)
658 .map(|(k, _)| k.clone())
659 else {
660 break;
661 };
662 state.negative_who_has.remove(&oldest);
663 }
664 state.negative_who_has.insert(target, now);
665 }
666
667 pub(crate) fn try_consume_who_has_response(
668 &self,
669 target: &WhoHasTarget,
670 received: &ReceivedApdu,
671 response_bytes: usize,
672 now: Instant,
673 ) -> bool {
674 let source_key = SourceKey::from_received(received);
675 let mut state = self.state.lock().unwrap();
676 let is_broadcast = received.is_group || received.link_layer_group;
677
678 if state.is_coalesced_who_has(&self.policy, target, &source_key, is_broadcast, now) {
679 self.counters.inc(&self.counters.requests_coalesced);
680 return false;
681 }
682
683 state.refill_global(&self.policy, now);
684 let is_reserved = is_source_reserved(&self.policy, &source_key, received);
685
686 if !state.has_global_capacity(&self.policy, is_reserved, response_bytes) {
687 self.counters.inc(&self.counters.responses_throttled_global);
688 return false;
689 }
690
691 let src = state.get_or_create_source_mut(&self.policy, &source_key, now);
692 if !src.has_capacity(&self.policy, response_bytes) {
693 self.counters.inc(&self.counters.responses_throttled_source);
694 return false;
695 }
696
697 src.deduct(&self.policy, response_bytes);
698 src.last_activity = now;
699 src.last_who_has.insert(target.clone(), now);
700 state.deduct_global(&self.policy, response_bytes);
701 if is_broadcast && !self.policy.prefer_directed_responses {
702 state.last_broadcast_who_has.insert(target.clone(), now);
703 }
704
705 true
706 }
707
708 pub(crate) fn record_i_have_sent(
709 &self,
710 actual_bytes: usize,
711 directed: bool,
712 target: &WhoHasTarget,
713 _source_mac: &MacAddr,
714 _source_network: Option<&NpduAddress>,
715 now: Instant,
716 ) {
717 self.counters.record_sent(actual_bytes, directed, false);
718
719 if !directed {
720 let mut state = self.state.lock().unwrap();
721 state.last_broadcast_who_has.insert(target.clone(), now);
722 }
723 }
724}
725
726fn is_source_reserved(policy: &DiscoveryPolicy, key: &SourceKey, recv: &ReceivedApdu) -> bool {
727 policy
728 .reserved_sources
729 .iter()
730 .any(|r| r == &key.mac || r == &recv.source_mac)
731}
732
733fn ensure_source_capacity(state: &mut DiscoveryState, policy: &DiscoveryPolicy, now: Instant) {
734 if state.sources.len() < policy.max_tracked_sources {
735 return;
736 }
737 state.sources.retain(|_, src| {
738 now.saturating_duration_since(src.last_activity) < Duration::from_secs(10)
739 });
740 while state.sources.len() >= policy.max_tracked_sources {
741 let Some(key) = state
742 .sources
743 .iter()
744 .min_by_key(|(_, src)| src.last_activity)
745 .map(|(k, _)| k.clone())
746 else {
747 break;
748 };
749 state.sources.remove(&key);
750 }
751}
752
753pub(crate) async fn broadcast_i_am_from<T: TransportPort + 'static>(
759 config: &ServerConfig,
760 db: &Arc<RwLock<ObjectDatabase>>,
761 network: &Arc<NetworkLayer<T>>,
762 comm_state: &CommState,
763 limiter: Option<&Arc<DiscoveryLimiter>>,
764) -> Result<(), Error> {
765 let guard = db.read().await;
766 let device_oid =
767 crate::local_device::validate_apdu_declaration(&guard, config.max_apdu_length)?
768 .ok_or_else(|| Error::Encoding("no Device object in database".into()))?;
769
770 let mut service_buf = BytesMut::new();
776 iam_request_for(device_oid, config).encode(&mut service_buf);
777
778 let mut buf = BytesMut::new();
779 encode_apdu(
780 &mut buf,
781 &Apdu::UnconfirmedRequest(UnconfirmedRequestPdu {
782 service_choice: UnconfirmedServiceChoice::I_AM,
783 service_request: service_buf.freeze(),
784 }),
785 )?;
786
787 drop(guard);
788 if comm_state.initiation_restricted() {
789 return Err(Error::Protocol {
790 class: ErrorClass::SERVICES.to_raw() as u32,
791 code: ErrorCode::COMMUNICATION_DISABLED.to_raw() as u32,
792 });
793 }
794 if let Some(limiter) = limiter {
795 limiter.record_i_am_sent(buf.len(), false, &MacAddr::new(), None, Instant::now());
796 }
797
798 network
799 .broadcast_apdu(&buf, false, NetworkPriority::NORMAL)
800 .await
801}
802
803#[doc(hidden)]
810pub fn iam_request_for(device_oid: ObjectIdentifier, config: &ServerConfig) -> IAmRequest {
811 IAmRequest {
812 object_identifier: device_oid,
813 max_apdu_length: config.max_apdu_length,
814 segmentation_supported: config.segmentation_supported,
815 vendor_id: config.vendor_id,
816 }
817}
818
819impl<T: TransportPort + 'static> BACnetServer<T> {
820 pub async fn broadcast_i_am(&self) -> Result<(), Error> {
825 self.broadcaster
826 .send(Some(Arc::clone(&self.discovery_limiter)))
827 .await
828 }
829}