bacnet_client/client/network_reports.rs
1//! Router discovery and network reports (Clause 6.4, #960): the client
2//! sends Who-Is-Router-To-Network and What-Is-Network-Number, and hands the
3//! I-Am-Router-To-Network, Network-Number-Is and Reject-Message-To-Network
4//! messages it receives to the subscribers of
5//! [`BACnetClient::network_reports`]. The client keeps the network-control
6//! channel: the dispatch task reads each control once, publishes its report,
7//! and then passes it on to the local Number controls or the routed-path
8//! limits as before.
9
10use super::i_have::collect_until;
11use super::*;
12use bacnet_network::layer::{NetworkReport, ReceivedNetworkControl, ReceivedNetworkReport};
13use bacnet_network::network_number::NumberControl;
14use bacnet_npdu::NetworkMessageType;
15use bacnet_router_core::message::Networks;
16
17/// Network-report broadcast channel capacity. A subscriber this far behind
18/// loses the oldest and is told how many with `RecvError::Lagged`; the
19/// dispatch task never waits for a subscriber.
20pub const NETWORK_REPORT_CHANNEL_CAPACITY: usize = 64;
21
22/// A router heard from during [`BACnetClient::discover_routers`].
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct DiscoveredRouter {
25 /// The router's MAC on the local network: the next hop to the networks
26 /// below.
27 pub mac: MacAddr,
28 /// Every network its I-Am-Router-To-Network messages listed, ascending,
29 /// each once.
30 pub networks: Vec<u16>,
31}
32
33/// Read `control` as a report and hand it to the subscribers; `number` is
34/// what [`NumberControl::parse`] made of it. With no subscriber nothing is
35/// parsed. Never waits.
36pub(super) fn publish(
37 tx: &broadcast::Sender<ReceivedNetworkReport>,
38 control: &ReceivedNetworkControl,
39 number: Option<NumberControl>,
40) {
41 if tx.receiver_count() == 0 {
42 return;
43 }
44 if let Some(report) = ReceivedNetworkReport::parse_with(control, number) {
45 let _ = tx.send(report);
46 }
47}
48
49/// Add one I-Am-Router-To-Network's `listed` networks to the entry for
50/// `mac`, made at the end when it is new.
51fn record(routers: &mut Vec<DiscoveredRouter>, mac: MacAddr, listed: Vec<u16>) {
52 let index = match routers.iter().position(|router| router.mac == mac) {
53 Some(index) => index,
54 None => {
55 routers.push(DiscoveredRouter {
56 mac,
57 networks: Vec::new(),
58 });
59 routers.len() - 1
60 }
61 };
62 let networks = &mut routers[index].networks;
63 networks.extend(listed);
64 networks.sort_unstable();
65 networks.dedup();
66}
67
68/// Refuse a network number no router can be asked about: 0, or 0xFFFF,
69/// the global broadcast.
70fn check_network(network: u16) -> Result<(), Error> {
71 if network == 0 || network == u16::MAX {
72 return Err(Error::Encoding(format!(
73 "network {network} is not a network number a router can reach; use 1..=65534"
74 )));
75 }
76 Ok(())
77}
78
79impl<T: TransportPort + 'static> BACnetClient<T> {
80 /// Get a receiver for the network reports this client receives: the
81 /// routers that announce themselves, Network-Number-Is broadcasts and
82 /// the rejects of messages it sent. Each call returns a new independent
83 /// receiver. A receiver that falls [`NETWORK_REPORT_CHANNEL_CAPACITY`]
84 /// behind loses the oldest.
85 pub fn network_reports(&self) -> broadcast::Receiver<ReceivedNetworkReport> {
86 self.network_report_tx.subscribe()
87 }
88
89 /// Send a Who-Is-Router-To-Network (Clause 6.4.1), asking which router
90 /// reaches `network`, or, for `None`, every router for all the networks
91 /// it reaches. It is broadcast on the local network when
92 /// `destination_mac` is `None`, else sent to that router alone. Answers
93 /// come as [`NetworkReport::IAmRouterToNetwork`] through
94 /// [`Self::network_reports`]. `network` 0 and 0xFFFF are refused.
95 pub async fn who_is_router_to_network(
96 &self,
97 network: Option<u16>,
98 destination_mac: Option<&[u8]>,
99 ) -> Result<(), Error> {
100 let mut payload = [0u8; 2];
101 let len = match network {
102 Some(network) => {
103 check_network(network)?;
104 Networks::write([network], &mut payload)
105 .map_err(|e| Error::Encoding(e.to_string()))?
106 }
107 None => 0,
108 };
109 self.network
110 .send_network_message(
111 NetworkMessageType::WHO_IS_ROUTER_TO_NETWORK,
112 &payload[..len],
113 destination_mac,
114 )
115 .await
116 }
117
118 /// Send a What-Is-Network-Number (Clause 6.4.19), broadcast when
119 /// `destination_mac` is `None`. Any node holding this network's number,
120 /// a router in particular, broadcasts a Network-Number-Is in reply,
121 /// which comes as [`NetworkReport::NetworkNumberIs`] through
122 /// [`Self::network_reports`]. Ask once and keep the answer: the
123 /// standard doesn't want the question repeated.
124 pub async fn what_is_network_number(
125 &self,
126 destination_mac: Option<&[u8]>,
127 ) -> Result<(), Error> {
128 self.network
129 .send_network_message(
130 NetworkMessageType::WHAT_IS_NETWORK_NUMBER,
131 &[],
132 destination_mac,
133 )
134 .await
135 }
136
137 /// Broadcast a Who-Is-Router-To-Network for `network` (or every
138 /// network), then collect the I-Am-Router-To-Network messages heard for
139 /// `wait`: one entry per router MAC, in the order first heard. For a
140 /// `network`, only the routers that list it are kept. No answer is an
141 /// empty list, not an error.
142 pub async fn discover_routers(
143 &self,
144 network: Option<u16>,
145 wait: Duration,
146 ) -> Result<Vec<DiscoveredRouter>, Error> {
147 // Subscribe first, so no answer can arrive before the receiver.
148 let mut rx = self.network_reports();
149 self.who_is_router_to_network(network, None).await?;
150 let deadline = tokio::time::Instant::now() + wait;
151 let mut routers: Vec<DiscoveredRouter> = Vec::new();
152 collect_until(&mut rx, deadline, "I-Am-Router-To-Network", |received| {
153 let NetworkReport::IAmRouterToNetwork(listed) = received.report else {
154 return;
155 };
156 if network.is_some_and(|wanted| !listed.contains(&wanted)) {
157 return;
158 }
159 record(&mut routers, received.source_mac, listed);
160 })
161 .await;
162 Ok(routers)
163 }
164}
165
166#[cfg(test)]
167#[path = "network_reports_tests.rs"]
168mod tests;