Skip to main content

bacnet_mstp_codec/
lib.rs

1//! An allocation-free codec for BACnet MS/TP frames (Clause 9): the standard
2//! frames and the two extended data frames.
3//!
4//! The crate covers the wire format and nothing above it: frame types, the
5//! header and data CRCs, the COBS encoding and CRC-32K of extended frames,
6//! one-shot encode and decode, and an incremental decoder for a serial byte
7//! stream. Token passing, timers and line control belong to the layer that
8//! uses it.
9//!
10//! The default build never allocates. [`Frame`] borrows its data,
11//! [`Frame::encode_into`] writes into caller storage, and [`StreamDecoder`]
12//! keeps its receive state in a buffer the caller lends it. Only the optional
13//! `alloc` and `bytes` sinks grow on the heap.
14//!
15//! # Extended frames
16//!
17//! Types 32 and 33 ([`FrameType::BACNET_EXTENDED_DATA_EXPECTING_REPLY`] and
18//! [`FrameType::BACNET_EXTENDED_DATA_NOT_EXPECTING_REPLY`]) carry NPDUs too long
19//! for a standard frame (Clauses 9.3.9, 9.3.10).
20//! On the wire their data is COBS-encoded with a CRC-32K, so no preamble can
21//! appear inside them (Clause 9.10), and the header's Length counts the two
22//! encoded fields less two, which lets a receiver without COBS skip them by
23//! count (Clause 9.3). [`Frame::data`] is always the decoded data: encoding
24//! takes it plain, and decoding hands it back plain. Decoding an extended
25//! frame writes its data over its encoded form, so it takes writable input:
26//! [`decode_frame_in_place`], or a [`StreamDecoder`], which does it in its
27//! own storage. [`decode_frame`] borrows read-only input and refuses an
28//! extended frame with [`FrameError::ExtendedFrame`] as soon as its header
29//! checks out.
30//!
31//! The other COBS-encoded types, 34-127, are reserved: both directions refuse
32//! them with [`FrameError::UnsupportedCobsFrame`], after the header's other
33//! checks. The exception is a [`StreamDecoder`] built
34//! [`with_reserved_cobs`](StreamDecoder::with_reserved_cobs), for a receiver
35//! that supports COBS: it checks their CRC-32K as an extended frame's and
36//! yields a good one as a frame of a type it doesn't know. Test_Request and
37//! Test_Response stay standard frames.
38//!
39//! # Validation
40//!
41//! Decoding is a raw, link-level check. [`decode_frame`] verifies the
42//! preamble, the header CRC, the header fields Clause 9.5.8 checks, then the
43//! frame's data. The header checks run in this order: the source, which may
44//! not be 255, the broadcast address no station may send from (Clause 9.3);
45//! the Length, at most [`MAX_STANDARD_MPDU_DATA`] on a standard type and
46//! within [`MIN_COBS_LENGTH`]..=[`MAX_COBS_LENGTH`] on a COBS-encoded one;
47//! and last whether the codec supports the type. Decoding accepts every other
48//! source, including the slave-only range 128-254: whether a station may use
49//! its address in its role is for the receiver to judge. It also accepts data
50//! on a frame type that never carries any, and a proprietary frame too short
51//! for its vendor identifier, so a receiver can see what arrived.
52//!
53//! Encoding is strict. [`Frame::encode_into`] and [`Frame::encode_to_sink`]
54//! apply the same header checks in the same order, then refuse data on the
55//! four frame types defined without any (see [`FrameType::allows_data`]) and
56//! a proprietary frame (128-255) with less than its 2-octet vendor identifier
57//! (Clause 9.3.11). The reserved types 8-31 get the same CRC, source and
58//! length checks as any frame in both directions, but the codec doesn't
59//! police their content. An extended frame needs data, since an empty one
60//! would fall below [`MIN_COBS_LENGTH`]; the BACnet range of 502 to
61//! [`MAX_EXTENDED_MPDU_DATA`] octets for it is the sending layer's to keep.
62//!
63//! # Storage
64//!
65//! Each frame class has a constant for its longest frame on the wire, which
66//! is the [`StreamDecoder`] storage it takes (plus room for the slices pushed
67//! into it):
68//!
69//! - [`MAX_STANDARD_FRAME_LENGTH`] (511): every standard frame.
70//! - [`MAX_EXTENDED_FRAME_LENGTH`] (1516): every extended frame that carries
71//!   a BACnet NPDU, at most [`MAX_EXTENDED_MPDU_DATA`] octets.
72//! - [`MAX_COBS_FRAME_LENGTH`] (2053): every frame whose header passes, a
73//!   COBS-encoded one at the largest Length included.
74//!
75//! A frame longer than the storage is reported once and skipped; see
76//! [`StreamDecoder`].
77//!
78//! # Features
79//!
80//! No feature needs `std`. Most add a [`FrameSink`] implementation:
81//!
82//! - `alloc`: for `alloc::vec::Vec<u8>`.
83//! - `bytes`: for `bytes::BytesMut`; implies `alloc`.
84//! - `heapless`: for `heapless::Vec<u8, N>`.
85//! - `crc32k-table`: works out the CRC-32K with a 256-entry table, about
86//!   1 KiB more flash than the default 16-entry one and half the lookups (see
87//!   [`crc::crc32k`]).
88//!
89//! # Example
90//!
91//! ```
92//! use bacnet_mstp_codec::{decode_frame, Frame, FrameType, MAX_STANDARD_FRAME_LENGTH};
93//!
94//! let frame = Frame::new(FrameType::BACNET_DATA_NOT_EXPECTING_REPLY, 0x10, 0x05, &[0x01, 0x00]);
95//! let mut wire = [0u8; MAX_STANDARD_FRAME_LENGTH];
96//! let length = frame.encode_into(&mut wire)?;
97//!
98//! let (decoded, consumed) = decode_frame(&wire[..length])?;
99//! assert_eq!(decoded, frame);
100//! assert_eq!(consumed, length);
101//! # Ok::<(), bacnet_mstp_codec::FrameError>(())
102//! ```
103
104#![no_std]
105#![cfg_attr(docsrs, feature(doc_cfg))]
106#![forbid(unsafe_code)]
107#![warn(missing_debug_implementations, clippy::must_use_candidate)]
108
109#[cfg(feature = "alloc")]
110extern crate alloc;
111
112mod cobs;
113pub mod crc;
114mod decode;
115mod error;
116mod frame;
117mod sink;
118mod stream;
119
120pub use decode::{decode_frame, decode_frame_in_place, find_preamble};
121pub use error::FrameError;
122pub use frame::{Frame, FrameHeader, FrameType};
123pub use sink::{FrameSink, SinkFull};
124pub use stream::{DecodeEvent, StreamDecoder, StreamPhase};
125
126/// The two preamble octets that start every frame.
127pub const PREAMBLE: [u8; 2] = [0x55, 0xFF];
128/// Header length after the preamble: type, destination, source, the two
129/// length octets and the header CRC.
130pub const HEADER_LENGTH: usize = 6;
131/// Largest data field a standard (non-COBS) frame can carry.
132pub const MAX_STANDARD_MPDU_DATA: usize = 501;
133/// Highest station address a master may use. Slaves may also use 128-254.
134pub const MAX_MASTER: u8 = 127;
135/// The broadcast address. It is valid only as a destination.
136pub const BROADCAST_MAC: u8 = 0xFF;
137/// Longest standard frame on the wire: preamble, header, the largest data
138/// field and its CRC.
139pub const MAX_STANDARD_FRAME_LENGTH: usize =
140    PREAMBLE.len() + HEADER_LENGTH + MAX_STANDARD_MPDU_DATA + 2;
141/// Smallest Length a COBS-encoded frame's header may declare: one data
142/// octet takes two encoded, the CRC field always takes five, and the Length
143/// leaves out two (Clause 9.5.3, Nmin_COBS_length).
144pub const MIN_COBS_LENGTH: usize = 5;
145/// Largest Length a COBS-encoded frame's header may declare (Clause 9.5.3,
146/// Nmax_COBS_length).
147pub const MAX_COBS_LENGTH: usize = 2043;
148/// Largest NPDU an extended frame carries (Clauses 9.3.9, 9.3.10).
149pub const MAX_EXTENDED_MPDU_DATA: usize = 1497;
150/// Longest extended frame carrying a BACnet NPDU: preamble, header, the
151/// largest NPDU with COBS's worst-case overhead of one octet per 254, and the
152/// Encoded CRC-32K (Clause 9.5.2 sizes a receive buffer the same way).
153pub const MAX_EXTENDED_FRAME_LENGTH: usize = PREAMBLE.len()
154    + HEADER_LENGTH
155    + MAX_EXTENDED_MPDU_DATA
156    + MAX_EXTENDED_MPDU_DATA.div_ceil(254)
157    + cobs::ENCODED_CRC_LENGTH;
158/// Longest frame whose header passes validation: a COBS-encoded frame of
159/// the largest Length.
160pub const MAX_COBS_FRAME_LENGTH: usize =
161    PREAMBLE.len() + HEADER_LENGTH + MAX_COBS_LENGTH + cobs::LENGTH_ADJUSTMENT;
162
163/// The README's example, compiled and run as a doctest.
164#[cfg(doctest)]
165#[doc = include_str!("../README.md")]
166pub struct ReadmeDoctests;