Skip to main content

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}