Expand description
A synchronous, platform-neutral BACnet MS/TP master core.
The core owns no clock, serial port, executor, lock, or system resource.
Callers supply timestamped events and execute the actions returned by
MasterCore::poll_action.
Time is an Instant in nanoseconds from an epoch the caller chooses,
with a Duration for lengths: bacnet-time’s types, re-exported here.
bacnet-bip-core and bacnet-router-core take the same ones, so firmware
that runs several of them feeds them one clock. A microsecond tick becomes
an instant with Instant::from_micros. The frame codec,
bacnet-mstp-codec, is re-exported as codec, since the API speaks
its types (FrameType, Frame,
FrameError).
The types that may grow are #[non_exhaustive]. Start a
MstpCoreConfig from MstpCoreConfig::STANDARD or
MstpCoreConfig::for_baud_rate and set the fields to change; a
QueueStorage of your own builds its items with
DequeuedNpdu::new and QueueItemInfo::new; and give a match on
an error, a refusal, a ReceiveError or a TransmitFailure a
wildcard arm. The enums an adapter acts on case by case,
Action, ReplyDecision and Direction, stay exhaustive, as do
the closed sets MasterState and QueueKind. Queues order by
bacnet-npdu’s closed NetworkPriority.
§Time
A now earlier than the latest one passed in is taken as that latest
one: time never runs backwards, so a clock that steps back holds the
timers still instead of failing. MasterCore::enqueue_npdu takes its
now the same way, though a later one it is given doesn’t move the
core’s clock: only the line, deadline, transmit and request calls do.
§Counters
MasterCore::take_counters returns what the core counted since the
last take, its queues’ counts with it, and starts the counts again from
zero. Each is a u32 that saturates between takes; add each take to
totals of your own, as wide as you need. The core keeps no totals, so a
firmware image carries no wider integers than it uses; the Tokio MS/TP
transport in bacnet-transport keeps u64s. MasterCore::counters
shows the same counts without taking them.
§The adapter’s side
MasterCore describes the calls in full. What the line does, the
deadlines included, goes in through on_* calls that cannot fail: an
input that breaks the core’s contract is ignored and counted in
MstpCounters::adapter_errors, and a broken internal invariant in
MstpCounters::internal_errors (debug builds assert). Only three
calls return an error, each its own: MasterCore::new a
ConfigError, MasterCore::on_transmit_started a
TransmitError, and MasterCore::complete_request a
RequestError. Three rules matter most:
- Poll after every call. After each call into the core, poll
MasterCore::poll_actionuntilAction::Idle, and always before reporting the next transmit event. Control actions can’t run out of room, and a start is refused (TransmitError::NotPolled) until its action has been polled; received data waits in a fixed number of slots, and data that finds them full is dropped and counted. - A frame’s octets come with its start.
Action::Transmitnames a frame; callMasterCore::on_transmit_startedright before enabling the line driver, and it returns the octets only if it confirms the start, so the line is never driven for a refused one. It refuses a start that comes within T_turnaround of line activity (TransmitError::Deferred), and the frame comes back later as a newAction::Transmit(Clause 9.5.5.1). - Echo begins at the confirmed start. Until then, keep the driver
off and the receiver on, and feed the core every octet and receive
error, draining the receive FIFO or DMA buffer right before the start
call: they are line activity. A
SetDirection(Receive)that arrives before you confirmed a start means the core took the frame back; forget its id. From the confirmed start toMasterCore::on_transmit_complete, what the receiver hears is the station’s own echo, which the core ignores; a frame it was part way through receiving is dropped then.
An adapter on a host serial API, which hands over octets in chunks
without per-octet times, still feeds every octet to
MasterCore::on_octet, stamped with the time of the read that returned
it: a stamp can only be late, which only defers the station’s own sends,
and the core’s own decoder then assembles every frame. That is what the
host MS/TP transport in bacnet-transport does, with a longer
T_frame_abort for chunk gaps. A backend that assembles frames in
hardware, and so has no octets to feed, reports them with
MasterCore::on_host_activity, MasterCore::on_validated_frame and
MasterCore::on_invalid_frame instead; the same rules apply, and such
an assembler drops its own partial frame at a confirmed start.
Line activity never makes the core abandon a frame it is waiting to send on its own: the frame waits for the line to go quiet. Only the evidence its state watches for, or a time limit taken from that state, drops it, and the drop is counted.
§Storage, and building the core on a microcontroller
The core’s large buffers belong to the caller, in one Storage
bundle: the receive storage, the transmit queues and the
ActionBuffers (the frame being sent and the delivery slots). The
core borrows the bundle for its lifetime, so what it holds itself is a
few hundred octets, and MasterCore::new returns it cheaply even on a
small stack. Name the bundle by its sizes, StandardStorage or
ExtendedStorage, and the sizes stay consistent with each other.
On firmware, keep the bundle in a static: StandardStorage::new is a
const fn of zeros, so the linker puts it in .bss and no flash holds
an initial image. Then build the core on the stack over it. This needs
no unsafe in the library and does not depend on the compiler
constructing anything in place:
use bacnet_mstp_core::{Instant, MasterCore, MstpCoreConfig, StandardStorage};
/// Four unicast and two broadcast queue slots, and one delivery slot:
/// this adapter polls after every call. On firmware it lives in a
/// static, borrowed once at reset (for example through cortex-m-rt's
/// `static mut`).
type Storage = StandardStorage<4, 2, 1>;
fn run(storage: &mut Storage, config: MstpCoreConfig, now: Instant) {
let Ok(mut core) = MasterCore::new(config, storage, now) else {
return;
};
// Feed the core the line's events and carry out its actions.
}A caller whose parts must live apart, such as queues shared with other
tasks or a receive storage of an unusual size, builds a Storage
around its queues with Storage::with_queues, or hands the core each
part with MasterCore::from_parts.
§Extended frames
MstpCoreConfig::extended_frames turns on the extended frame types,
32 and 33, which carry NPDUs of 502 to 1497 octets COBS-encoded. Clause
9.3 requires them of a routing node and leaves them optional for any
other, so the flag is off by default, and a core without it behaves as
one built before it existed. With it on, every buffer must hold an
extended frame, as an ExtendedStorage does: receive storage of
MAX_EXTENDED_FRAME_LENGTH,
ExtendedActionBuffers, queues whose slots hold
MAX_EXTENDED_MPDU_DATA
octets, and so a MasterCore<'_, Q, MAX_EXTENDED_MPDU_DATA>.
MasterCore::new refuses smaller ones, naming the buffer. A core
built over an ExtendedStorage with the flag off uses a standard
frame’s share of the receive storage. The sizes are const generics with
standard defaults, so a firmware image that leaves the flag off costs no
more RAM or flash than before: its buffers keep their standard sizes,
and the flag’s branches are small.
§A local policy
When a NO_TOKEN deadline is serviced after this station’s token
generation slot has closed, the core opens the window again a cycle of
slots later rather than wait for ever, which Clause 9.5.6.7 does not
provide for. Each token generated that way is counted in
MstpCounters::missed_generation_slots; see MasterCore.
§Queue storage, and the features
FixedQueues keeps the transmit queues in arrays sized at compile
time, with no allocator and no feature. The alloc feature adds
AllocQueues, which reserves its capacity when built; no feature needs
std. A caller may also implement QueueStorage itself.
The inspect feature adds a hidden view of a frame handed over and not
yet started, for a test oracle that checks what the core will send. It
is a test and fuzz hook, not covered by semver. An adapter never needs
it: it gets the octets from MasterCore::on_transmit_started.
Re-exports§
pub use bacnet_mstp_codec as codec;pub use bacnet_npdu as npdu;
Structs§
- Action
Buffers - The storage behind the actions a
MasterCorereturns, which the core borrows for as long as it lives, as it borrows its receive storage. - Alloc
Queues alloc - Allocation-backed queues. Capacity is reserved by the constructor, so enqueue and dequeue do not grow a vector after initialization.
- Dequeued
Npdu - An NPDU taken off its queue, with its octets lent by the queue.
- Duration
- A length of time, in nanoseconds on a
u64: at most about 584 years. - Fixed
Queues - Fixed-capacity, independently bounded unicast and broadcast queues.
- Instant
- A point on the caller’s monotonic timeline, in nanoseconds since an epoch the caller chooses.
- Lateness
Histogram - How late serviced deadlines were, in fixed buckets, and the latest.
- Master
Core - Synchronous, allocation-free MS/TP master core.
- Master
Snapshot - Copyable state for diagnostics without exposing mutable internals.
- Mstp
Core Config - All timing, addressing, and bounded-queue parameters needed by an MS/TP master.
- Mstp
Counters - Everything the master core counts, since the counters were last taken
(
MasterCore::take_counters). - Queue
Counters - Counters for one bounded transmit queue, since they were last taken
(
QueueStorage::take_counters). - Queue
Item Info - A non-mutating view of the next item selected by queue policy.
- Request
Id - A generation-bound identifier for an application request.
- Storage
- A core’s receive storage, transmit queues and
ActionBuffers, sized together, whichMasterCore::newborrows for the core’s lifetime. - Transmit
Id - A generation-bound identifier for a transmit operation.
Enums§
- Action
- An operation emitted by the core.
- Config
Error - A configuration value that violates a protocol or local safety bound.
- Core
Buffer - The buffers
ConfigError::ExtendedStorageTooSmallnames. - Direction
- The physical direction requested for the serial transceiver.
- Local
Duration Field - Names of local duration settings used by
ConfigError::ZeroLocalDuration. - Local
Limit Field - Names of local count/ratio settings used by
ConfigError::ZeroLocalLimit. - Master
State - Token-passing master state names from Clause 9.5.6.
- Queue
Kind - Destination class used by the two independently bounded queues.
- Queue
Refusal - Why a queue, or
MasterCore::enqueue_npdu, refused an NPDU. - Receive
Error - UART receive failures that count as bus activity.
- Reply
Decision - Application decision for a request-bearing delivery.
- Request
Error - Why
MasterCore::complete_requestrefused a decision. - Transmit
Error - Why
MasterCore::on_transmit_startedrefused a start. The adapter sends nothing for that id; onlySelf::NotPolledleaves it live. - Transmit
Failure - Reason a platform could not complete a transmit operation.
Constants§
- DEFAULT_
DELIVERY_ SLOTS - The delivery slots
ActionBuffershas unless the caller picks another number. - DEFAULT_
LATE_ DEADLINE - Default for
MstpCoreConfig::late_deadline. - LATENESS_
BUCKETS - The number of buckets in a
LatenessHistogram. - MAX_
FRAME_ ABORT - The longest T_frame_abort a station may use (Clause 9.5.3): 100 ms.
MstpCoreConfig::validaterefuses a longer one. - MAX_
REPLY_ TIMEOUT - The longest T_reply_timeout a station may use (Clause 9.5.3): 300 ms.
MstpCoreConfig::validaterefuses a longer one. - MAX_
USAGE_ TIMEOUT - The longest T_usage_timeout a station may use (Clause 9.5.3): 35 ms.
MstpCoreConfig::validaterefuses a longer one. - MIN_
REPLY_ TIMEOUT - The shortest T_reply_timeout a station may use (Clause 9.5.3): 255 ms,
the baud-rate constructors’ value.
MstpCoreConfig::validaterefuses a shorter one. No requester stops waiting for a reply sooner, so the core also uses it to bound how late its own answer may start. - MIN_
RX_ STORAGE - The smallest receive storage
MasterCore::newaccepts: a preamble and a header, the size of the shortest frame. - MIN_
USAGE_ TIMEOUT - The shortest T_usage_timeout a station may use (Clause 9.5.3): 20 ms, the
baud-rate constructors’ value.
MstpCoreConfig::validaterefuses a shorter one. No poller stops waiting for the answer to its poll sooner, so the core also uses it to bound how late its own answer may start. - N_
MIN_ OCTETS - Nmin_octets (Clause 9.5.3): a station that hears more receive events (octets and receive errors) than this takes the line to be in use, 4. Fixed by the standard.
- N_POLL
- Npoll (Clause 9.5.3): the token uses between two maintenance Poll For Master cycles, 50. Fixed by the standard, so not a configuration field.
- N_
RETRY_ TOKEN - Nretry_token (Clause 9.5.3): how many times the core sends its Token again to a successor that shows no sign of using it before it looks for another, once. Fixed by the standard.
- T_
NO_ TOKEN - T_no_token (Clause 9.5.3): the silence after which a station takes the token to be lost, 500 ms. Fixed by the standard.
- T_SLOT
- T_slot (Clause 9.5.3): the width of each station’s turn to generate a lost token, 10 ms. Fixed by the standard.
Traits§
- Queue
Storage - A bounded storage backend for queued NPDUs: the core’s transmit queues.
Type Aliases§
- Extended
Action Buffers ActionBufferssized for extended frames, asMstpCoreConfig::extended_framesneeds: delivery slots ofMAX_EXTENDED_MPDU_DATAoctets and a frame buffer ofMAX_EXTENDED_FRAME_LENGTH.- Extended
Storage Storagefor a core withMstpCoreConfig::extended_frameson: the same asStandardStorage, with every buffer sized for extended frames (MAX_EXTENDED_MPDU_DATAoctets of data,MAX_EXTENDED_FRAME_LENGTHon the wire).- Standard
Storage Storagefor a core without extended frames:FixedQueuesofUunicast andBbroadcast slots,DELIVERIESdelivery slots, all for standard frames of up to 501 octets of data.