bacnet_mstp_core/lib.rs
1//! A synchronous, platform-neutral BACnet MS/TP master core.
2//!
3//! The core owns no clock, serial port, executor, lock, or system resource.
4//! Callers supply timestamped events and execute the actions returned by
5//! [`MasterCore::poll_action`].
6//!
7//! Time is an [`Instant`] in nanoseconds from an epoch the caller chooses,
8//! with a [`Duration`] for lengths: `bacnet-time`'s types, re-exported here.
9//! `bacnet-bip-core` and `bacnet-router-core` take the same ones, so firmware
10//! that runs several of them feeds them one clock. A microsecond tick becomes
11//! an instant with [`Instant::from_micros`]. The frame codec,
12//! `bacnet-mstp-codec`, is re-exported as [`codec`], since the API speaks
13//! its types ([`FrameType`](codec::FrameType), [`Frame`](codec::Frame),
14//! [`FrameError`](codec::FrameError)).
15//!
16//! The types that may grow are `#[non_exhaustive]`. Start a
17//! [`MstpCoreConfig`] from [`MstpCoreConfig::STANDARD`] or
18//! [`MstpCoreConfig::for_baud_rate`] and set the fields to change; a
19//! [`QueueStorage`] of your own builds its items with
20//! [`DequeuedNpdu::new`] and [`QueueItemInfo::new`]; and give a `match` on
21//! an error, a refusal, a [`ReceiveError`] or a [`TransmitFailure`] a
22//! wildcard arm. The enums an adapter acts on case by case,
23//! [`Action`], [`ReplyDecision`] and [`Direction`], stay exhaustive, as do
24//! the closed sets [`MasterState`] and [`QueueKind`]. Queues order by
25//! `bacnet-npdu`'s closed [`NetworkPriority`](npdu::NetworkPriority).
26//!
27//! # Time
28//!
29//! A `now` earlier than the latest one passed in is taken as that latest
30//! one: time never runs backwards, so a clock that steps back holds the
31//! timers still instead of failing. [`MasterCore::enqueue_npdu`] takes its
32//! `now` the same way, though a later one it is given doesn't move the
33//! core's clock: only the line, deadline, transmit and request calls do.
34//!
35//! # Counters
36//!
37//! [`MasterCore::take_counters`] returns what the core counted since the
38//! last take, its queues' counts with it, and starts the counts again from
39//! zero. Each is a `u32` that saturates between takes; add each take to
40//! totals of your own, as wide as you need. The core keeps no totals, so a
41//! firmware image carries no wider integers than it uses; the Tokio MS/TP
42//! transport in `bacnet-transport` keeps `u64`s. [`MasterCore::counters`]
43//! shows the same counts without taking them.
44//!
45//! # The adapter's side
46//!
47//! [`MasterCore`] describes the calls in full. What the line does, the
48//! deadlines included, goes in through `on_*` calls that cannot fail: an
49//! input that breaks the core's contract is ignored and counted in
50//! [`MstpCounters::adapter_errors`], and a broken internal invariant in
51//! [`MstpCounters::internal_errors`] (debug builds assert). Only three
52//! calls return an error, each its own: [`MasterCore::new`] a
53//! [`ConfigError`], [`MasterCore::on_transmit_started`] a
54//! [`TransmitError`], and [`MasterCore::complete_request`] a
55//! [`RequestError`]. Three rules matter most:
56//!
57//! - **Poll after every call.** After each call into the core, poll
58//! [`MasterCore::poll_action`] until [`Action::Idle`], and always before
59//! reporting the next transmit event. Control actions can't run out of
60//! room, and a start is refused ([`TransmitError::NotPolled`]) until
61//! its action has been polled; received data waits in a fixed number of
62//! slots, and data that finds them full is dropped and counted.
63//! - **A frame's octets come with its start.** [`Action::Transmit`] names
64//! a frame; call [`MasterCore::on_transmit_started`] right before
65//! enabling the line driver, and it returns the octets only if it
66//! confirms the start, so the line is never driven for a refused one. It
67//! refuses a start that comes within T_turnaround of line activity
68//! ([`TransmitError::Deferred`]), and the frame comes back later as a
69//! new [`Action::Transmit`] (Clause 9.5.5.1).
70//! - **Echo begins at the confirmed start.** Until then, keep the driver
71//! off and the receiver on, and feed the core every octet and receive
72//! error, draining the receive FIFO or DMA buffer right before the start
73//! call: they are line activity. A `SetDirection(Receive)` that arrives
74//! before you confirmed a start means the core took the frame back;
75//! forget its id. From the confirmed start to [`MasterCore::on_transmit_complete`],
76//! what the receiver hears is the station's own echo, which the core
77//! ignores; a frame it was part way through receiving is dropped then.
78//!
79//! An adapter on a host serial API, which hands over octets in chunks
80//! without per-octet times, still feeds every octet to
81//! [`MasterCore::on_octet`], stamped with the time of the read that returned
82//! it: a stamp can only be late, which only defers the station's own sends,
83//! and the core's own decoder then assembles every frame. That is what the
84//! host MS/TP transport in `bacnet-transport` does, with a longer
85//! T_frame_abort for chunk gaps. A backend that assembles frames in
86//! hardware, and so has no octets to feed, reports them with
87//! [`MasterCore::on_host_activity`], [`MasterCore::on_validated_frame`] and
88//! [`MasterCore::on_invalid_frame`] instead; the same rules apply, and such
89//! an assembler drops its own partial frame at a confirmed start.
90//!
91//! Line activity never makes the core abandon a frame it is waiting to send
92//! on its own: the frame waits for the line to go quiet. Only the evidence
93//! its state watches for, or a time limit taken from that state, drops it,
94//! and the drop is counted.
95//!
96//! # Storage, and building the core on a microcontroller
97//!
98//! The core's large buffers belong to the caller, in one [`Storage`]
99//! bundle: the receive storage, the transmit queues and the
100//! [`ActionBuffers`] (the frame being sent and the delivery slots). The
101//! core borrows the bundle for its lifetime, so what it holds itself is a
102//! few hundred octets, and [`MasterCore::new`] returns it cheaply even on a
103//! small stack. Name the bundle by its sizes, [`StandardStorage`] or
104//! [`ExtendedStorage`], and the sizes stay consistent with each other.
105//!
106//! On firmware, keep the bundle in a static: [`StandardStorage::new`] is a
107//! `const fn` of zeros, so the linker puts it in `.bss` and no flash holds
108//! an initial image. Then build the core on the stack over it. This needs
109//! no `unsafe` in the library and does not depend on the compiler
110//! constructing anything in place:
111//!
112//! ```
113//! use bacnet_mstp_core::{Instant, MasterCore, MstpCoreConfig, StandardStorage};
114//!
115//! /// Four unicast and two broadcast queue slots, and one delivery slot:
116//! /// this adapter polls after every call. On firmware it lives in a
117//! /// static, borrowed once at reset (for example through cortex-m-rt's
118//! /// `static mut`).
119//! type Storage = StandardStorage<4, 2, 1>;
120//!
121//! fn run(storage: &mut Storage, config: MstpCoreConfig, now: Instant) {
122//! let Ok(mut core) = MasterCore::new(config, storage, now) else {
123//! return;
124//! };
125//! // Feed the core the line's events and carry out its actions.
126//! # let _ = core.poll_action();
127//! }
128//! # run(&mut Storage::new(), MstpCoreConfig::STANDARD, Instant::ZERO);
129//! ```
130//!
131//! A caller whose parts must live apart, such as queues shared with other
132//! tasks or a receive storage of an unusual size, builds a [`Storage`]
133//! around its queues with [`Storage::with_queues`], or hands the core each
134//! part with [`MasterCore::from_parts`].
135//!
136//! # Extended frames
137//!
138//! [`MstpCoreConfig::extended_frames`] turns on the extended frame types,
139//! 32 and 33, which carry NPDUs of 502 to 1497 octets COBS-encoded. Clause
140//! 9.3 requires them of a routing node and leaves them optional for any
141//! other, so the flag is off by default, and a core without it behaves as
142//! one built before it existed. With it on, every buffer must hold an
143//! extended frame, as an [`ExtendedStorage`] does: receive storage of
144//! [`MAX_EXTENDED_FRAME_LENGTH`](codec::MAX_EXTENDED_FRAME_LENGTH),
145//! [`ExtendedActionBuffers`], queues whose slots hold
146//! [`MAX_EXTENDED_MPDU_DATA`](codec::MAX_EXTENDED_MPDU_DATA)
147//! octets, and so a `MasterCore<'_, Q, MAX_EXTENDED_MPDU_DATA>`.
148//! [`MasterCore::new`] refuses smaller ones, naming the buffer. A core
149//! built over an [`ExtendedStorage`] with the flag off uses a standard
150//! frame's share of the receive storage. The sizes are const generics with
151//! standard defaults, so a firmware image that leaves the flag off costs no
152//! more RAM or flash than before: its buffers keep their standard sizes,
153//! and the flag's branches are small.
154//!
155//! # A local policy
156//!
157//! When a NO_TOKEN deadline is serviced after this station's token
158//! generation slot has closed, the core opens the window again a cycle of
159//! slots later rather than wait for ever, which Clause 9.5.6.7 does not
160//! provide for. Each token generated that way is counted in
161//! [`MstpCounters::missed_generation_slots`]; see [`MasterCore`].
162//!
163//! # Queue storage, and the features
164//!
165//! [`FixedQueues`] keeps the transmit queues in arrays sized at compile
166//! time, with no allocator and no feature. The `alloc` feature adds
167//! `AllocQueues`, which reserves its capacity when built; no feature needs
168//! `std`. A caller may also implement [`QueueStorage`] itself.
169//!
170//! The `inspect` feature adds a hidden view of a frame handed over and not
171//! yet started, for a test oracle that checks what the core will send. It
172//! is a test and fuzz hook, not covered by semver. An adapter never needs
173//! it: it gets the octets from [`MasterCore::on_transmit_started`].
174
175#![no_std]
176#![forbid(unsafe_code)]
177#![cfg_attr(docsrs, feature(doc_cfg))]
178#![warn(missing_debug_implementations, clippy::must_use_candidate)]
179
180#[cfg(feature = "alloc")]
181extern crate alloc;
182#[cfg(test)]
183extern crate std;
184
185mod actions;
186mod config;
187mod counters;
188mod errors;
189mod queue;
190mod state;
191mod storage;
192
193pub use actions::{
194 Action, ActionBuffers, DEFAULT_DELIVERY_SLOTS, Direction, ExtendedActionBuffers, RequestId,
195 TransmitFailure, TransmitId,
196};
197pub use bacnet_mstp_codec as codec;
198/// `bacnet-npdu`, the network-layer vocabulary: the core queues each NPDU
199/// at its [`NetworkPriority`](npdu::NetworkPriority).
200pub use bacnet_npdu as npdu;
201pub use bacnet_time::{Duration, Instant};
202
203// An MS/TP extended frame carries the longest NPDU any data link carries.
204const _: () = assert!(
205 codec::MAX_EXTENDED_MPDU_DATA == npdu::MAX_NPDU,
206 "the MS/TP codec's extended-frame limit is bacnet-npdu's MAX_NPDU"
207);
208pub use config::{
209 ConfigError, CoreBuffer, DEFAULT_LATE_DEADLINE, LocalDurationField, LocalLimitField,
210 MAX_FRAME_ABORT, MAX_REPLY_TIMEOUT, MAX_USAGE_TIMEOUT, MIN_REPLY_TIMEOUT, MIN_USAGE_TIMEOUT,
211 MstpCoreConfig, N_MIN_OCTETS, N_POLL, N_RETRY_TOKEN, T_NO_TOKEN, T_SLOT,
212};
213pub use counters::{LATENESS_BUCKETS, LatenessHistogram, MstpCounters, QueueCounters};
214pub use errors::{RequestError, TransmitError};
215#[cfg(feature = "alloc")]
216pub use queue::AllocQueues;
217pub use queue::{DequeuedNpdu, FixedQueues, QueueItemInfo, QueueKind, QueueRefusal, QueueStorage};
218pub use state::{
219 MIN_RX_STORAGE, MasterCore, MasterSnapshot, MasterState, ReceiveError, ReplyDecision,
220};
221pub use storage::{ExtendedStorage, StandardStorage, Storage};
222
223/// The README's example, compiled and run as a doctest.
224#[cfg(doctest)]
225#[doc = include_str!("../README.md")]
226#[derive(Debug)]
227pub struct ReadmeDoctests;