Skip to main content

Crate bacnet_mstp_core

Crate bacnet_mstp_core 

Source
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_action until Action::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::Transmit names a frame; call MasterCore::on_transmit_started right 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 new Action::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 to MasterCore::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§

ActionBuffers
The storage behind the actions a MasterCore returns, which the core borrows for as long as it lives, as it borrows its receive storage.
AllocQueuesalloc
Allocation-backed queues. Capacity is reserved by the constructor, so enqueue and dequeue do not grow a vector after initialization.
DequeuedNpdu
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.
FixedQueues
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.
LatenessHistogram
How late serviced deadlines were, in fixed buckets, and the latest.
MasterCore
Synchronous, allocation-free MS/TP master core.
MasterSnapshot
Copyable state for diagnostics without exposing mutable internals.
MstpCoreConfig
All timing, addressing, and bounded-queue parameters needed by an MS/TP master.
MstpCounters
Everything the master core counts, since the counters were last taken (MasterCore::take_counters).
QueueCounters
Counters for one bounded transmit queue, since they were last taken (QueueStorage::take_counters).
QueueItemInfo
A non-mutating view of the next item selected by queue policy.
RequestId
A generation-bound identifier for an application request.
Storage
A core’s receive storage, transmit queues and ActionBuffers, sized together, which MasterCore::new borrows for the core’s lifetime.
TransmitId
A generation-bound identifier for a transmit operation.

Enums§

Action
An operation emitted by the core.
ConfigError
A configuration value that violates a protocol or local safety bound.
CoreBuffer
The buffers ConfigError::ExtendedStorageTooSmall names.
Direction
The physical direction requested for the serial transceiver.
LocalDurationField
Names of local duration settings used by ConfigError::ZeroLocalDuration.
LocalLimitField
Names of local count/ratio settings used by ConfigError::ZeroLocalLimit.
MasterState
Token-passing master state names from Clause 9.5.6.
QueueKind
Destination class used by the two independently bounded queues.
QueueRefusal
Why a queue, or MasterCore::enqueue_npdu, refused an NPDU.
ReceiveError
UART receive failures that count as bus activity.
ReplyDecision
Application decision for a request-bearing delivery.
RequestError
Why MasterCore::complete_request refused a decision.
TransmitError
Why MasterCore::on_transmit_started refused a start. The adapter sends nothing for that id; only Self::NotPolled leaves it live.
TransmitFailure
Reason a platform could not complete a transmit operation.

Constants§

DEFAULT_DELIVERY_SLOTS
The delivery slots ActionBuffers has 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::validate refuses a longer one.
MAX_REPLY_TIMEOUT
The longest T_reply_timeout a station may use (Clause 9.5.3): 300 ms. MstpCoreConfig::validate refuses a longer one.
MAX_USAGE_TIMEOUT
The longest T_usage_timeout a station may use (Clause 9.5.3): 35 ms. MstpCoreConfig::validate refuses 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::validate refuses 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::new accepts: 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::validate refuses 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§

QueueStorage
A bounded storage backend for queued NPDUs: the core’s transmit queues.

Type Aliases§

ExtendedActionBuffers
ActionBuffers sized for extended frames, as MstpCoreConfig::extended_frames needs: delivery slots of MAX_EXTENDED_MPDU_DATA octets and a frame buffer of MAX_EXTENDED_FRAME_LENGTH.
ExtendedStorage
Storage for a core with MstpCoreConfig::extended_frames on: the same as StandardStorage, with every buffer sized for extended frames (MAX_EXTENDED_MPDU_DATA octets of data, MAX_EXTENDED_FRAME_LENGTH on the wire).
StandardStorage
Storage for a core without extended frames: FixedQueues of U unicast and B broadcast slots, DELIVERIES delivery slots, all for standard frames of up to 501 octets of data.