Skip to main content

bacnet_bip_codec/
lib.rs

1//! An allocation-free codec for BACnet/IP BVLL messages, on IPv4 (Annex J)
2//! at the crate root and on IPv6 (Annex U) in [`ipv6`].
3//!
4//! The crate covers the wire format and nothing above it. For B/IP that is
5//! the 4-octet BVLL header, the twelve messages Annex J.2 defines, six-octet
6//! B/IP addresses (J.1.2), and the entries of BDTs and FDTs; [`ipv6`] does
7//! the same for the twelve messages of Annex U.2, with B/IPv6 addresses and
8//! virtual MAC addresses. Sockets, BBMD forwarding, the foreign-device
9//! registration lifecycle and which device answers what belong to the layer
10//! that uses it. Both message families share the [`BvllSink`] trait and the
11//! header constants.
12//!
13//! The default build never allocates. [`Bvll`] borrows its NPDU or table
14//! from the datagram, tables are read through iterators
15//! ([`Entries::iter`]), and [`Bvll::encode_into`] writes into caller
16//! storage. Only the optional `alloc` and `bytes` sinks grow on the heap.
17//!
18//! # Validation
19//!
20//! Decoding is strict about shape and lenient about values.
21//! [`BvllFrame::decode`] requires BVLC type 0x81 and a Length field equal to
22//! the datagram's length: the field counts the whole message (J.2), so a
23//! truncated datagram and one with trailing octets are both refused.
24//! [`BvllFrame::message`] then requires a function J.2 defines and a payload
25//! of the length its format clause gives (see [`BvlcFunction::payload_shape`]).
26//! It does not judge field values: an undefined result code, a zero
27//! time-to-live, any table entry and an empty NPDU all decode, and the
28//! receiver decides what to do with them. [`decode`] runs both steps.
29//!
30//! Function 0x0C is unknown here: the clause that once defined it (J.2.13)
31//! is deleted in this edition.
32//!
33//! Encoding is checked. A [`Bvll`] variant holds exactly its function's
34//! fields, so most malformed shapes can't be built; [`Bvll::encoded_len`]
35//! also refuses a message longer than the Length field can state and a
36//! BVLC-Result with a code J.2.1.1 doesn't define.
37//!
38//! # Addresses
39//!
40//! A B/IP address is a plain [`SocketAddrV4`](core::net::SocketAddrV4), and
41//! [`address_to_octets`], [`address_from_octets`] and
42//! [`address_from_slice`] convert it to and from its six wire octets. Every
43//! field of a `SocketAddrV4` travels, so a type of its own would add
44//! nothing. B/IPv6 does get one, [`ipv6::Bip6Address`]: a `SocketAddrV6`
45//! also holds a flow label and a scope ID, which never go on the wire, and
46//! the type keeps only the 18 octets that do.
47//!
48//! # Features
49//!
50//! No feature needs `std`. Each adds a [`BvllSink`] implementation, which
51//! both encoders use:
52//!
53//! - `alloc`: for `alloc::vec::Vec<u8>`.
54//! - `bytes`: for `bytes::BytesMut`; implies `alloc`.
55//! - `heapless`: for `heapless::Vec<u8, N>`.
56//!
57//! # Example
58//!
59//! ```
60//! use bacnet_bip_codec::{decode, Bvll, BvllError, BvlcFunction};
61//!
62//! let npdu = [0x01, 0x20, 0xFF, 0xFF, 0x00, 0xFF];
63//! let message = Bvll::OriginalBroadcastNpdu(&npdu);
64//! let mut wire = [0u8; 64];
65//! let length = message.encode_into(&mut wire)?;
66//! assert_eq!(wire[..4], [0x81, 0x0B, 0x00, 0x0A]);
67//!
68//! let decoded = decode(&wire[..length])?;
69//! assert_eq!(decoded, message);
70//! assert_eq!(decoded.function(), BvlcFunction::ORIGINAL_BROADCAST_NPDU);
71//! assert_eq!(decoded.npdu(), Some(&npdu[..]));
72//! # Ok::<(), BvllError>(())
73//! ```
74
75#![no_std]
76#![cfg_attr(docsrs, feature(doc_cfg))]
77#![forbid(unsafe_code)]
78#![warn(missing_debug_implementations, clippy::must_use_candidate)]
79
80#[cfg(feature = "alloc")]
81extern crate alloc;
82
83mod error;
84mod function;
85pub mod ipv6;
86mod message;
87mod sink;
88mod table;
89
90pub use error::BvllError;
91pub use function::{BvlcFunction, BvlcResultCode, PayloadShape};
92pub use message::{decode, Bvll, BvllFrame};
93pub use sink::{BvllSink, SinkFull};
94pub use table::{
95    address_from_octets, address_from_slice, address_to_octets, BdtEntries, BdtEntry, Entries,
96    EntryIter, FdtEntries, FdtEntry, TableEntry,
97};
98
99/// The BVLC Type octet of a B/IP message (Annex J.2).
100pub const BVLC_TYPE_BIP: u8 = 0x81;
101/// The BVLL header: type, function and the two Length octets. B/IP and
102/// B/IPv6 share it (Annex J.2, U.2).
103pub const HEADER_LENGTH: usize = 4;
104/// The largest message the 16-bit Length field can state, header included,
105/// in either annex.
106pub const MAX_BVLL_LENGTH: usize = u16::MAX as usize;
107/// Octets in a B/IP address: IPv4 address and UDP port (Annex J.1.2).
108pub const BIP_ADDRESS_LENGTH: usize = 6;
109/// Octets in a broadcast distribution table entry: B/IP address and mask
110/// (Annex J.2.2.1).
111pub const BDT_ENTRY_LENGTH: usize = 10;
112/// Octets in a foreign device table entry: B/IP address, time-to-live and
113/// seconds remaining (Annex J.2.8.1).
114pub const FDT_ENTRY_LENGTH: usize = 10;
115/// The default UDP port, 47808 (0xBAC0), for B/IP and B/IPv6 alike (Annex
116/// J.1.2, U.1.1.2).
117pub const DEFAULT_PORT: u16 = 0xBAC0;
118
119/// The README's example, compiled and run as a doctest.
120#[cfg(doctest)]
121#[doc = include_str!("../README.md")]
122pub struct ReadmeDoctests;