bacnet_bip_core/node/send.rs
1//! What a node sends for an NPDU: the message and its destination, within
2//! the datagram budget.
3
4use core::fmt;
5use core::net::SocketAddrV4;
6
7use bacnet_bip_codec::{Bvll, HEADER_LENGTH, MAX_BVLL_LENGTH};
8
9use bacnet_npdu::MAX_NPDU;
10
11use super::config::{Link, Mode};
12
13/// A message to send. The caller encodes it
14/// ([`Bvll::encode_into`](bacnet_bip_codec::Bvll::encode_into)) and sends
15/// it from the node's own socket to `to`.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[non_exhaustive]
18pub struct Outgoing<'n> {
19 /// The destination B/IP address.
20 pub to: SocketAddrV4,
21 /// The message.
22 pub message: Bvll<'n>,
23}
24
25/// How an NPDU is addressed.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Cast {
28 /// To one node.
29 Unicast,
30 /// To every node on the network.
31 Broadcast,
32}
33
34/// Why a send was refused. Nothing went out.
35///
36/// The link refuses an NPDU it can't carry, a foreign device's node a
37/// broadcast it can't hold, and the host's socket, through a port's
38/// [`Datagrams`](crate::port::Datagrams), a datagram it can't take.
39///
40/// It is `#[non_exhaustive]`, so a later reason isn't a breaking change: a
41/// `match` needs a wildcard arm.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43#[non_exhaustive]
44pub enum SendRefusal {
45 /// The NPDU is longer than [`Link::max_npdu`] allows.
46 TooLong {
47 /// The longest NPDU this cast takes.
48 max: usize,
49 },
50 /// A foreign device holds as many broadcasts as its caller can keep
51 /// ([`Foreign::hold`](super::Foreign::hold)) until its first
52 /// registration attempt is answered.
53 HoldFull,
54 /// A unicast to one of the link's group addresses
55 /// ([`Link::groups`](super::Link::groups)): the subnet's broadcast
56 /// address, the limited broadcast or a multicast address. An
57 /// Original-Unicast-NPDU goes to one node's own address; to reach the
58 /// group, broadcast ([`Link::broadcast`]).
59 GroupDestination,
60 /// The host's socket can't take the datagram now: its queue is full.
61 SocketFull,
62 /// The host's socket couldn't send the datagram: its buffer can't hold
63 /// the encoded message, or the send failed.
64 SocketError,
65}
66
67impl fmt::Display for SendRefusal {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match self {
70 Self::TooLong { max } => write!(f, "NPDU longer than the {max} octets B/IP sends"),
71 Self::HoldFull => f.write_str("no room to hold a broadcast until registration"),
72 Self::GroupDestination => {
73 f.write_str("a unicast can't go to a group address; send a broadcast instead")
74 }
75 Self::SocketFull => f.write_str("the socket can't take the datagram now"),
76 Self::SocketError => f.write_str("the socket couldn't send the datagram"),
77 }
78 }
79}
80
81impl core::error::Error for SendRefusal {}
82
83/// Octets a broadcast's NPDU gains, at worst, before it reaches the
84/// network's devices: the 4-octet header, and the 6-octet origin a BBMD
85/// adds when it forwards it.
86const BROADCAST_OVERHEAD: usize = HEADER_LENGTH + 6;
87
88impl Link {
89 /// The longest NPDU sent as `cast`.
90 ///
91 /// With a budget, a unicast's datagram is the NPDU plus the 4-octet
92 /// header, and a broadcast's, once a BBMD forwards it as a
93 /// Forwarded-NPDU, 6 octets more; either way at most [`MAX_NPDU`]
94 /// (Table 6-1). Without one, what the Length field can state.
95 #[must_use]
96 pub fn max_npdu(&self, cast: Cast) -> usize {
97 let overhead = match cast {
98 Cast::Unicast => HEADER_LENGTH,
99 Cast::Broadcast => BROADCAST_OVERHEAD,
100 };
101 match self.budget {
102 Some(budget) => usize::from(budget).saturating_sub(overhead).min(MAX_NPDU),
103 None => MAX_BVLL_LENGTH - overhead,
104 }
105 }
106
107 /// An Original-Unicast-NPDU carrying `npdu` to `to` (Annex J.3).
108 ///
109 /// `to` must be one node's address: a group address of this link
110 /// ([`Groups::contains`](super::Groups::contains)) is refused with
111 /// [`SendRefusal::GroupDestination`]. A receiver that checks the
112 /// destination drops an Original-Unicast-NPDU sent to a group, so it
113 /// would reach no one, or nodes that should have ignored it (#1677).
114 /// Another subnet's directed broadcast address isn't known here, so it
115 /// isn't caught; a receiver's destination check still drops it.
116 pub fn unicast<'n>(
117 &self,
118 to: SocketAddrV4,
119 npdu: &'n [u8],
120 ) -> Result<Outgoing<'n>, SendRefusal> {
121 if self.groups().contains(to) {
122 return Err(SendRefusal::GroupDestination);
123 }
124 self.fits(npdu, Cast::Unicast)?;
125 Ok(Outgoing {
126 to,
127 message: Bvll::OriginalUnicastNpdu(npdu),
128 })
129 }
130
131 /// A local broadcast of `npdu`, by mode: an Original-Broadcast-NPDU to
132 /// the subnet's broadcast address for a normal node or a BBMD (Annex
133 /// J.4.1), and a Distribute-Broadcast-To-Network to its BBMD for a
134 /// foreign device (Annex J.2.10). The flag is set for a BBMD, which
135 /// also forwards its own broadcast
136 /// ([`Tables::own_broadcast`](crate::bbmd::Tables::own_broadcast)).
137 ///
138 /// A foreign device asks [`Node::admit_broadcast`](super::Node::admit_broadcast)
139 /// first.
140 pub fn broadcast<'n>(&self, npdu: &'n [u8]) -> Result<(Outgoing<'n>, bool), SendRefusal> {
141 self.fits(npdu, Cast::Broadcast)?;
142 let local = Outgoing {
143 to: self.subnet.broadcast,
144 message: Bvll::OriginalBroadcastNpdu(npdu),
145 };
146 Ok(match self.mode {
147 Mode::Normal => (local, false),
148 Mode::Bbmd => (local, true),
149 Mode::Foreign(foreign) => (
150 Outgoing {
151 to: foreign.bbmd,
152 message: Bvll::DistributeBroadcastToNetwork(npdu),
153 },
154 false,
155 ),
156 })
157 }
158
159 fn fits(&self, npdu: &[u8], cast: Cast) -> Result<(), SendRefusal> {
160 if self.budget.is_none() {
161 return Ok(());
162 }
163 let max = self.max_npdu(cast);
164 if npdu.len() > max {
165 return Err(SendRefusal::TooLong { max });
166 }
167 Ok(())
168 }
169}