Skip to main content

Crate bacnet_router_core

Crate bacnet_router_core 

Source
Expand description

Portable BACnet router behaviour (Clause 6), for firmware and for the hosted router of bacnet-network.

The crate is no_std and needs no allocator. NPDUs are read in place from the caller’s frame and written into the caller’s buffer; the routing table, the discovery state and the router’s output scratch live in arrays whose size the caller picks with const generics; and time is an Instant the caller passes to each call that needs one. It reads no clock and opens no socket, and nothing on a routing decision formats text or divides a 64-bit value. None of that storage keeps timers or defaults of its own, so it all starts as zero bits and can live in .bss.

Instant and Duration are bacnet-time’s, in nanoseconds, re-exported here: the types bacnet-mstp-core and bacnet-bip-core take too, so firmware that runs a router between their ports feeds all three one clock.

  • npdu: bacnet-npdu, re-exported: the network-layer vocabulary the router speaks. Its borrowed view of an NPDU’s control information is the stack’s one NPDU codec, whose encoder refuses an NPDU too long for its buffer before writing an octet; it also has the message type codes, the reject reasons, the network priority, the port limits and the bounded Mac.
  • message: the network-layer messages a router reads and sends: allocation-free views and writers for their payloads.
  • table: the routing table, sorted by network number, with learning, the convergence modes, busy and unreachable marks, the reject hold-down and aging.
  • discovery: which unknown networks to look for, at most once per cooldown each, in bounded storage.
  • router: the sans-IO engine that joins them: a Router over a Storage takes each frame a port receives, says what it means for the router’s own application, and asks for the sends it calls for, which the caller polls. It takes the time with every call.
  • node: the receive side of a node that doesn’t route: the checks every arriving frame passes, which the router runs too, and what a node keeps of what passes. The hosted non-router network layer runs on it.

A port is identified by its index, a PortId; its MAC is a Mac of up to MAX_MAC_LEN octets. To the engine a port is data, so any link plugs in; two features add the glue for the portable link cores, so firmware can route between them with no allocator:

  • bip: B/IP ports over bacnet-bip-core’s port driver, in normal or foreign-device mode, or as a BBMD that answers management requests from its tables (module bip).
  • mstp: MS/TP ports over bacnet-mstp-core’s master core, with extended frames (module mstp).

Both hand the router each NPDU their link delivers as an Arrived, whose frame borrows its link source, and carry out the router’s sends on their link. Each module documents its wiring, and the clauses its tests cover. Each feature also re-exports its link core (bip_core, mstp_core, and through them their codecs), so a router image depends on this crate alone, and RouterIn names a router over borrowed storage without repeating its sizes.

The router counts what it drops, sends and is refused as the link cores count: Router::take_counters drains u32 counts that saturate between takes, for the caller to add to totals as wide as it needs, and Router::counters looks without taking.

The types that may grow are #[non_exhaustive]. Start the Options from STANDARD (or HOSTED, the hosted router’s, with its 256-route cap) and set the fields to change; build a Route, for comparison, with its direct or learned, and counters from Default; and give a match on an error, a refusal, a Discard, an Event, a Kind or another of these enums a wildcard arm. The enums a host acts on case by case, Action, Received and a node’s Received, stay exhaustive, as do the closed sets such as Dest, Group and Reachability. The wire codes, NetworkMessageType and RejectMessageReason, are open: any octet converts, so a match on their constants needs a wildcard arm too.

§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. Here that is the Router; the table and discovery keep no clock of their own, and count a now before a stamp as no time passed. The B/IP and MS/TP ports hand their now to their link core, which keeps the same rule, and a BBMD port keeps the latest time for its tables too.

§Example

A router between network 1, a B/IP-like link with six-octet MACs, and network 2, an MS/TP-like one, passes a request from station 5 on network 2 on to a node on network 1:

use bacnet_router_core::npdu::{Address, Mac, Npdu, NpduLimits};
use bacnet_router_core::router::{
    Action, Dest, Frame, Groups, Options, Permissive, PortConfig, Received,
};
use bacnet_router_core::{Instant, PortId, Router, Storage};

let ports = [
    PortConfig::new(1, Mac::from_array([192, 0, 2, 1, 0xBA, 0xC0]), NpduLimits::both(1497), Groups::None),
    PortConfig::new(2, Mac::from_array([1]), NpduLimits::both(1497), Groups::None),
];
// Two ports, a table of 8 routes and 8 hold-down records; firmware
// keeps the storage in a static.
let mut storage = Storage::<2, 8, 8>::new();
let mut router = Router::new(&mut storage, &ports, Options::STANDARD, Instant::ZERO)?;

// A confirmed request for the node at 192.0.2.9 on network 1.
let node = [192, 0, 2, 9, 0xBA, 0xC0];
let mut request = Npdu::apdu(&[0x00, 0x05, 0x01, 0x0C]);
request.destination = Some(Address { network: 1, mac: &node });
request.expecting_reply = true;
let mut frame = [0u8; 32];
let len = request.encode_into(&mut frame)?;

let arrived = Frame { port: PortId(1), source: &[5], link_group: false, npdu: &frame[..len] };
let received = router.receive(arrived, Instant::from_secs(1), &mut Permissive);
assert_eq!(received, Received::Handled);

// It goes to the node on port 0, with the station as its source.
let Action::Send(send) = router.poll_action() else {
    panic!("a send");
};
assert_eq!((send.port, send.to), (PortId(0), Dest::Unicast(&node[..])));
let relayed = Npdu::decode(send.npdu).expect("an NPDU");
assert_eq!(relayed.source, Some(Address { network: 2, mac: &[5] }));
assert_eq!(relayed.destination, None);
assert_eq!(router.poll_action(), Action::Idle);

§Clause 6 coverage

What the core is answerable for, in our words, with the tests that show it. First its parts:

ObligationClauseTest
The control octet says which NPCI fields follow, and they follow in one fixed order; a zero DNET, a zero or all-ones SNET and an empty SADR make the NPDU invalid6.2, 6.2.2, 6.2.2.1bacnet-npdu’s tests/view.rs: decoded_frames_are_consistent, malformations_are_named
A DLEN or SLEN past any data link’s address width is told apart from other faults, carrying the DNET and the source a reason-6 reject needs6.2.2.2, 6.4.4bacnet-npdu’s tests/view.rs: over_long_addresses_carry_what_a_reject_needs
Each network-layer message’s payload is read whole or not at all, and written in the layout the clause gives it6.4tests/message.rs
The table records, for each network it can reach, the port, the next router (none for a directly attached network), and a state that separates a lasting failure from congestion6.6.1tests/table.rs
A later claim of the route to a network supersedes the earlier one6.6.3.2tests/table_claims.rs
A reject with reason 1 marks a learned route unreachable, and one with reason 2 marks it busy for a while; no other reason changes the table6.6.3.5src/table/tests/rejects.rs (unit tests, as the hold-down records they read are the crate’s own)
Flow control holds traffic for the networks named, or for every network the sending router serves when none are named, and lets it go again6.6.3.6, 6.6.3.7tests/table_busy.rs
A routing-table update replaces or adds a network’s row, and Port ID 0 deletes it6.4.7, 6.6.3.8tests/table.rs: management_updates_never_touch_direct_routes
A router with no route to a network looks for one before it refuses the message6.6.3.1tests/discovery.rs

Then the engine, with the hosted router’s options:

ObligationClauseTest
An address length no link uses draws reason 6 when the NPDU names a network, and nothing when it is a global broadcast6.2.2.2, 6.4.4tests/ingress.rs: over_long_addresses_reject_with_reason_6
A message for the router’s own address on any of its networks goes to its own application, not on a wire6.1, Figure 6-12tests/ingress.rs: own_address_on_any_port_is_local, own_mac_on_a_remote_network_is_routed_on
Only an Unconfirmed-Request may use a broadcast network address6.3tests/ingress.rs: broadcast_addresses_carry_only_unconfirmed
Missing SNET and SADR are filled in from the arrival network and the link sender; present ones travel unchanged6.5.4tests/forward.rs: source_is_added_once_and_kept_after
Onto an attached network the destination fields and hop count are dropped, and the frame goes to the DADR, or to every node for DLEN 06.5.4tests/forward.rs: direct_delivery_strips_destination
Toward another router the destination fields stay and the hop count falls by one; a count that would reach zero goes no further6.2.3, 6.5.4tests/forward.rs: next_router_keeps_destination_and_counts_down, hop_count_reaching_zero_is_not_relayed, hop_count_reaching_zero_stops_routed_network_messages
A global broadcast reaches every attached network but the one it came from, still global, and the router’s own application too; one whose count would reach zero is only delivered6.3.2, 6.5.4tests/forward.rs: global_broadcast_fans_out_and_delivers
A network message for another network is routed like an APDU, a proprietary one included, vendor ID and all6.2.5, 6.5.4tests/forward.rs: proprietary_messages_route_opaquely
Lacking a route, the router searches for one and refuses the message with reason 16.6.3.1tests/traces.rs: a_discovery_learning_and_aging
A refusal goes to whoever first sent the refused message, as its source fields say6.4.4, 6.5.4tests/reject.rs: reject_path_follows_the_origin
A reject that arrives updates the table, for reasons 1 and 2 only, then travels on toward the node it names, whatever its reason6.6.3.5tests/reject.rs: received_rejects_relay_by_their_destination, other_reasons_relay_and_keep_the_route; tests/traces.rs: c_received_rejects
Traffic for a network on hold draws reason 2 until the hold lapses or is lifted6.6.3.6tests/traces.rs: b_flow_control
A message type the router doesn’t know, sent to it, draws reason 36.6.3.5tests/control.rs: unknown_types_draw_reason_3
Route announcements teach the table and go on to the other ports6.6.3.2, 6.6.3.3tests/control.rs: i_am_router_learns_then_rebroadcasts; tests/traces.rs: d_hardened_convergence
A route question about one network is answered when the network lies beyond another port, searched further, with the asker as origin, when the table lacks it, and left alone when it lies beyond the arrival port6.6.3.2tests/control.rs: scoped_who_is_router_cases
A route question about every network is answered with those beyond the other ports, ascending, but for those failed for good6.6.3.2tests/control.rs: unscoped_answer_lists_reachable_networks
Flow control covers what its sender serves, named or not, and goes on to the other ports6.6.3.6, 6.6.3.7tests/control.rs: flow_control_covers_what_the_sender_serves
A table update applies entry by entry and gets an empty answer; a query gets the whole table, over several answers when needed, and changes nothing6.4.7, 6.6.3.8, 6.6.3.9tests/init.rs
The network-number messages stay on their link6.4.19, 6.4.20tests/control.rs: network_number_messages_stay_local
No port sits on network 0, which names none, or 0xFFFF, which can’t be the SNET of what it relays6.2.2.1tests/engine.rs: new_refuses_what_it_cannot_route
At start each port hears its own number, then which networks lie beyond the others6.4.20, 6.6.2, 6.6.3.3tests/traces.rs: a_discovery_learning_and_aging, a_one_port_router_announces_only_its_number
With no point-to-point links, connection messages change nothing6.6.3.10, 6.6.3.11tests/control.rs: ptp_messages_change_nothing
A message too long for its egress port draws reason 4, a remote broadcast included, and a fan-out leaves that port out6.4.4, 6.6.3.5, Table 6-1tests/forward.rs: too_long_for_the_egress_port_draws_reason_4, a_fan_out_goes_by_the_broadcast_limit
One port’s sends leave in network-priority order: a frame’s own output, then the router’s own messages at Normal priority6.5.4tests/congestion.rs: one_ports_sends_leave_in_priority_order
The router’s own application reaches another network through the table, from the address it selects, and its replies come back to it; with no route it searches first6.1, 6.5.4, 6.6.3.1tests/originate.rs (#1699)
The router’s own global broadcast leaves by every port as that link’s broadcast, still global with a full hop count, from the selected address on each6.2.3, 6.3.2, 6.5.3, 6.5.4tests/originate_global.rs (#1809)

With the opt-in Congestion policy (#1697):

ObligationClauseTest
A port that stays clogged has its networks announced busy on every other port, again while it lasts, and available once it drains6.6.3.6, 6.6.3.7, 6.6.4tests/congestion.rs: busy_after_clog_time_then_available_under_the_low_water_mark, a_lasting_clog_repeats_busy_and_drains_once
Traffic a clogged port’s full queue refuses draws reason 26.6.3.6tests/congestion.rs: a_full_queue_on_a_clogged_port_draws_reason_2

The other opt-in hardening of #1698, beyond the clause: with next_hop_rejects, a reject changes a learned route only when its next router sent it (tests/hardening.rs: next_hop_rejects_apply_only_from_the_routes_next_router); flow control from any other router is counted, as it always was ignored (flow_control_from_another_router_is_counted_and_changes_nothing); and with duplicates, a global broadcast seen again within its time to live is dropped (a_looped_copy_within_the_ttl_is_dropped), and so is a relayed I-Am-Router, Router-Busy, Router-Available or Who-Is-Router, which no hop count bounds: around a loop of routers one circulates for good without it, and with it each router takes the first copy only (tests/loops.rs, #1810, #2020). Without any option, a global broadcast or Who-Is-Router whose SNET and SADR name the router itself is its own come back round a loop, and is dropped (tests/loops.rs, #2021).

Rules of this stack beyond the clause, each tested in the same files: the 18-octet address bound (#1141, #1198); a DADR beside DNET 0xFFFF (#1379); group DADRs, where only an Unconfirmed-Request for a group of the delivery port’s own goes on, as that port’s broadcast (#1504, #1677); the relay back out the arrival port of a message for the arrival network (a DLEN 0 broadcast at the port’s limit is delivered and also draws reason 4, as the relay gains source fields); the RB-09 gate for each protected message, silent when it refuses; direct routes that no learning, reject or management write changes; a 30-second hold-down per network and arrival port on reject-driven changes; hardened convergence, which waits for a second claim before moving a route to another port; the route cap; a discovery cooldown, cap and expiry; no reject for a global broadcast, and none to the router itself.

§Features

None by default: the crate then depends only on bacnet-time and bacnet-npdu. Neither feature needs std or an allocator, and every combination builds on bare-metal targets.

  • bip: the B/IP glue, which adds bacnet-bip-core, and through it bacnet-bip-codec, re-exported as bip_core.
  • mstp: the MS/TP glue, which adds bacnet-mstp-core, re-exported as mstp_core.

Re-exports§

pub use router::Router;
pub use router::RouterIn;
pub use router::Storage;
pub use bacnet_bip_core as bip_core;bip
pub use bacnet_mstp_core as mstp_core;mstp
pub use bacnet_npdu as npdu;

Modules§

bipbip
B/IP ports: the router’s adapter over bacnet-bip-core’s port driver (feature bip).
discovery
Looking for routes to unknown networks, in bounded storage.
message
The network-layer messages a router reads and sends (Clause 6.4).
mstpmstp
MS/TP ports: the glue between a Router and bacnet-mstp-core’s master core (feature mstp).
node
The receive side of the network layer for a node that doesn’t route (Clause 6.5.2): which arriving NPDUs reach its application or its network-message consumer, and which are dropped.
router
The router engine: Clause 6 routing between ports, with no I/O.
table
The routing table (Clause 6.6.1), in caller-sized storage.

Structs§

Arrivedbip or mstp
An NPDU a port’s link core delivered, with its link source held by value, so the Frame the router reads can borrow it.
Duration
A length of time, in nanoseconds on a u64: at most about 584 years.
Instant
A point on the caller’s monotonic timeline, in nanoseconds since an epoch the caller chooses.
PortId
A router port, by its index in the router’s port list.