1use super::dispatch_context::{DispatchContext, InboundApdu};
2use super::*;
3use std::sync::atomic::{AtomicU64, Ordering};
4
5const DEVICE_PURGE_INTERVAL: Duration = Duration::from_secs(300);
6#[cfg(not(test))]
7const DEVICE_MAX_AGE: Duration = Duration::from_secs(600);
8#[cfg(test)]
12const DEVICE_MAX_AGE: Duration = Duration::ZERO;
13
14impl ClientConfig {
15 pub(super) fn validate_request_settings(&self) -> Result<(), Error> {
16 validate_max_apdu_length(self.max_apdu_length)?;
17 validate_max_segments(self.max_segments)?;
18 pacing::validate_interval_ms(self.min_request_interval_ms)
19 }
20
21 pub(super) fn validate_window(&self) -> Result<(), Error> {
22 if !(1..=127).contains(&self.proposed_window_size) {
23 return Err(Error::Encoding(format!(
24 "invalid proposed-window-size {}; expected 1..=127",
25 self.proposed_window_size
26 )));
27 }
28 Ok(())
29 }
30}
31
32impl<T: TransportPort> BACnetClient<T> {
33 fn abort_owned_tasks(&self) {
34 for task in [&self.dispatch_task, &self.network_number_task]
35 .into_iter()
36 .flatten()
37 {
38 task.abort();
39 }
40 }
41}
42
43async fn join_owned_task(slot: &mut Option<JoinHandle<()>>) {
44 if let Some(task) = slot.as_mut() {
46 let _ = task.await;
47 }
48 *slot = None;
49}
50
51impl<T: TransportPort + 'static> BACnetClient<T> {
52 pub async fn start(config: ClientConfig, transport: T) -> Result<Self, Error> {
54 Self::start_with_options(config, transport, ClientOptions::default()).await
55 }
56
57 pub async fn start_with_options(
59 mut config: ClientConfig,
60 transport: T,
61 options: ClientOptions,
62 ) -> Result<Self, Error> {
63 config.validate_request_settings()?;
64 config.max_apdu_length =
65 cap_max_apdu_to_transport(config.max_apdu_length, transport.egress_apdu_limit())?;
66 config.validate_window()?;
67 options.validate()?;
68
69 let mut network = NetworkLayer::new(transport);
70 let mut network_control_rx = network.enable_network_control_receiver()?;
71 let mut apdu_rx = network.start().await?;
72 config.max_apdu_length = cap_max_apdu_to_transport(
73 config.max_apdu_length,
74 network.transport().egress_apdu_limit(),
75 )?;
76 let local_mac = MacAddr::from_slice(network.local_mac());
77
78 let network = Arc::new(network);
79 let (number_tx, network_number_task) = if network
80 .transport()
81 .supports_local_nonrouter_number_controls()
82 {
83 let (tx, task) = network_number::spawn(&network);
84 (Some(tx), Some(task))
85 } else {
86 (None, None)
87 };
88
89 let coordinator = Arc::new(OutboundTransactionCoordinator::new());
90 let tsm = Arc::new(Mutex::new(new_coordinated_tsm(&config, coordinator)));
91 let tsm_dispatch = Arc::clone(&tsm);
92 let device_table = Arc::new(Mutex::new(DeviceTable::new()));
93 let device_table_dispatch = Arc::clone(&device_table);
94 let network_dispatch = Arc::clone(&network);
95 let (cov_tx, _) =
96 broadcast::channel::<ReceivedCOVNotification>(options.cov_channel_capacity);
97 let cov_tx_dispatch = cov_tx.clone();
98 let (event_tx, _) =
99 broadcast::channel::<ReceivedEventNotification>(options.event_channel_capacity);
100 let event_tx_dispatch = event_tx.clone();
101 let confirmed_cov_ack_policy = options.confirmed_cov_notification_ack_policy.clone();
102 let (device_tx, _) = broadcast::channel::<DeviceEvent>(DEVICE_EVENT_CHANNEL_CAPACITY);
103 let device_tx_dispatch = device_tx.clone();
104 let (device_collision_tx, _) =
105 broadcast::channel::<DeviceCollisionEvent>(DEVICE_EVENT_CHANNEL_CAPACITY);
106 let device_collision_tx_dispatch = device_collision_tx.clone();
107 let (i_have_tx, _) = broadcast::channel::<ReceivedIHave>(I_HAVE_CHANNEL_CAPACITY);
108 let i_have_tx_dispatch = i_have_tx.clone();
109 let (network_report_tx, _) = broadcast::channel(NETWORK_REPORT_CHANNEL_CAPACITY);
110 let network_report_tx_dispatch = network_report_tx.clone();
111 let seg_ack_senders: Arc<Mutex<HashMap<SegAckKey, SegmentAckRoute>>> =
112 Arc::new(Mutex::new(HashMap::new()));
113 let seg_ack_senders_dispatch = Arc::clone(&seg_ack_senders);
114 let (cleanup_tx, mut cleanup_rx) = mpsc::unbounded_channel::<TransactionCleanup>();
115 #[cfg(test)]
116 let segmented_cleanup = Arc::new(SegmentedCleanupHook::default());
117 #[cfg(test)]
118 let segmented_cleanup_dispatch = Arc::clone(&segmented_cleanup);
119 let response_limits = ResponseLimits::from_config(&config);
120 let routed_path_limits = Arc::new(RoutedPathLimits::new(routed_path_quarantine_horizon(
121 &config,
122 )));
123 let routed_path_limits_dispatch = Arc::clone(&routed_path_limits);
124 let group_source_request_drops = Arc::new(AtomicU64::new(0));
125 let group_source_drops_dispatch = Arc::clone(&group_source_request_drops);
126 let decode_warning = Arc::new(WarnThrottle::default());
130 let response_warning = Arc::new(WarnThrottle::default());
131 #[cfg(test)]
132 let decode_warning_hook = Arc::clone(&decode_warning);
133 #[cfg(test)]
134 let response_warning_hook = Arc::clone(&response_warning);
135
136 let dispatch_task = tokio::spawn(async move {
137 let mut seg_state: HashMap<SegKey, SegmentedReceiveState> = HashMap::new();
138 let mut device_purge_interval = tokio::time::interval(DEVICE_PURGE_INTERVAL);
139 let mut network_control_open = true;
140 device_purge_interval.tick().await;
143
144 loop {
145 tokio::select! {
146 control = network_control_rx.recv(), if network_control_open => {
147 match control {
148 Some(control) => {
149 let number = bacnet_network::network_number::NumberControl::parse(&control);
152 network_reports::publish(&network_report_tx_dispatch, &control, number);
153 if let Some((tx, parsed)) = number_tx.as_ref().zip(number)
154 {
155 if tx.try_send(parsed).is_err() {
156 debug!("client Number control queue full or closed; dropping control");
157 }
158 } else {
159 routed_path_limits_dispatch
161 .handle_network_control(&tsm_dispatch, control)
162 .await;
163 }
164 }
165 None => network_control_open = false,
166 }
167 }
168 cleanup = cleanup_rx.recv() => {
169 let Some(cleanup) = cleanup else {
170 break;
171 };
172 if cleanup.cancel_tsm {
173 tsm_dispatch.lock().await.cancel_transaction_for_owner(
174 &cleanup.mac,
175 cleanup.invoke_id,
176 &cleanup.owner,
177 );
178 }
179 let found = seg_state
183 .keys()
184 .find(|key| {
185 key.0 == cleanup.mac
186 && key.1 == cleanup.invoke_id
187 && seg_state
188 .get(*key)
189 .is_some_and(|state| {
190 state.owner.same_as(&cleanup.owner)
191 })
192 })
193 .cloned();
194 #[cfg(test)]
195 let removed = found.is_some();
196 if let Some(found) = found {
197 seg_state.remove(&found);
198 }
199 if let Some(expected_sender) = cleanup.seg_ack_sender {
200 let key = (cleanup.mac, cleanup.invoke_id);
201 let mut senders = seg_ack_senders_dispatch.lock().await;
202 if senders
203 .get(&key)
204 .is_some_and(|route| {
205 route.owner.same_as(&cleanup.owner)
206 && route.sender.same_channel(&expected_sender)
207 })
208 {
209 senders.remove(&key);
210 }
211 }
212 #[cfg(test)]
213 segmented_cleanup_dispatch.record_processed(removed);
214 }
215 _ = device_purge_interval.tick() => {
216 let lost_devices = device_table_dispatch
217 .lock()
218 .await
219 .purge_stale_collect(DEVICE_MAX_AGE);
220 for device in lost_devices {
221 let _ = device_tx_dispatch.send(DeviceEvent {
222 kind: DeviceEventKind::Lost,
223 device,
224 });
225 }
226 }
227 received = apdu_rx.recv() => {
228 let Some(received) = received else {
229 break;
230 };
231 match apdu::decode_apdu(received.apdu.clone()) {
232 Ok(Apdu::ConfirmedRequest(_))
235 if network_dispatch
236 .transport()
237 .is_group_destination(&received.source_mac) =>
238 {
239 group_source_drops_dispatch.fetch_add(1, Ordering::Relaxed);
240 debug!("Ignoring a ConfirmedRequest from a group address");
241 }
242 Ok(decoded) => {
243 Self::dispatch_apdu(
244 DispatchContext {
245 tsm: &tsm_dispatch,
246 device_table: &device_table_dispatch,
247 network: &network_dispatch,
248 cov_tx: &cov_tx_dispatch,
249 event_tx: &event_tx_dispatch,
250 confirmed_cov_ack_policy: &confirmed_cov_ack_policy,
251 device_tx: &device_tx_dispatch,
252 device_collision_tx: &device_collision_tx_dispatch,
253 i_have_tx: &i_have_tx_dispatch,
254 seg_ack_senders: &seg_ack_senders_dispatch,
255 decode_warning: &decode_warning,
256 response_warning: &response_warning,
257 },
258 &mut seg_state,
259 InboundApdu {
260 source_mac: &received.source_mac,
261 source_network: &received.source_network,
262 provenance: received.provenance,
263 response_route: received.response_route(),
264 is_group: received.is_group,
265 reply_tx: received.reply_tx,
266 },
267 decoded,
268 response_limits,
269 )
270 .await;
271 }
272 Err(e) => {
273 warn_throttled!(
274 decode_warning,
275 error = %e,
276 "Failed to decode received APDU"
277 );
278 }
279 }
280 }
281 }
282 }
283 });
284
285 let pacer = Arc::new(pacing::RequestPacer::new(Duration::from_millis(
287 config.min_request_interval_ms,
288 )));
289 Ok(Self {
290 config,
291 network,
292 tsm,
293 device_table,
294 cov_tx,
295 event_tx,
296 device_tx,
297 device_collision_tx,
298 i_have_tx,
299 network_report_tx,
300 dispatch_task: Some(dispatch_task),
301 network_number_task,
302 seg_ack_senders,
303 cleanup_tx,
304 #[cfg(test)]
305 segmented_post_wait_cleanup: Arc::new(SegmentedPostWaitCleanupHook::default()),
306 #[cfg(test)]
307 segmented_cleanup,
308 #[cfg(test)]
309 decode_warning: decode_warning_hook,
310 #[cfg(test)]
311 response_warning: response_warning_hook,
312 local_mac,
313 routed_path_limits,
314 group_source_request_drops,
315 pacer,
316 })
317 }
318
319 pub fn group_source_request_drops(&self) -> u64 {
327 self.group_source_request_drops.load(Ordering::Relaxed)
328 }
329 pub fn local_mac(&self) -> &[u8] {
331 &self.local_mac
332 }
333
334 pub fn max_apdu_length(&self) -> u16 {
340 max_apdu_bucket_at_or_below(
341 self.config
342 .max_apdu_length
343 .min(self.network.transport().egress_apdu_limit()),
344 )
345 .unwrap_or(0)
346 }
347
348 pub fn transport_max_apdu_length(&self) -> u16 {
354 self.network.transport().egress_apdu_limit()
355 }
356
357 pub async fn stop(&mut self) -> Result<(), Error> {
360 self.abort_owned_tasks();
361 join_owned_task(&mut self.network_number_task).await;
362 join_owned_task(&mut self.dispatch_task).await;
363 self.tsm.lock().await.cancel_all_transactions();
364 let network = Arc::get_mut(&mut self.network).ok_or_else(|| {
365 Error::Encoding("cannot stop BACnetClient while network references remain".into())
366 })?;
367 network.stop().await?;
368 Ok(())
369 }
370}
371
372impl<T: TransportPort> Drop for BACnetClient<T> {
373 fn drop(&mut self) {
374 self.abort_owned_tasks();
375 if let Ok(mut tsm) = self.tsm.try_lock() {
376 tsm.cancel_all_transactions();
377 }
378 }
379}