The Core Layer: Frame Flow

The core layer owns four concerns: what an identifier means, how outbound frames are queued, how payload bytes are composed and consumed, and how numbers are represented on the wire. The identifier layout, the priority values and the number encoding are specified in the Protocol Specification §5, §6 and §9 and are not repeated here. What follows is the part that is not in any specification: how the layer behaves.

1. The identifier is the routing table

Everything a receiver needs in order to decide what a frame is — its urgency, its functional group, its specific message, and who it is for — lives in the identifier. No routing decision requires reading a payload.

Three properties follow, and they are why the layout is worth its complexity:

The layout is known in exactly one place. Composition and extraction helpers are compile-time evaluable, so identifiers known at build time — an acceptance filter table, for example — cost nothing at run time. Category code never manipulates the bits itself; that is what makes the layout changeable in one place.

The command/response split in the message-type field is the other structural decision: it lets a server discard responses without a lookup, which is what stops two servers on one bus from reacting to each other’s answers.

2. One outbound queue per node

The bus interface accepts one frame at a time. Without a queue, a category wanting to send two frames back to back would have to know whether the controller was busy — so the core layer owns that knowledge, once, for the whole node.

Arbitration only orders frames that have reached the controller. A queue that released frames first-in-first-out would let an emergency frame sit behind telemetry that was queued earlier, and a queue full of telemetry would refuse it outright. The queue therefore sorts what it holds by the identifier’s priority and keeps part of its capacity for emergency traffic alone. The admission and retry rules are REQ-CAN-039 to REQ-CAN-041 in the Protocol Requirements.

The behaviours of that machine that the rest of the library relies on:

Behaviour Why it matters
The queue advances before the finished frame’s completion runs A completion that itself sends finds the transport in a consistent state, so chained sends are safe
Queued frames leave in priority order, ties in arrival order Once the frame on the bus completes, an emergency frame goes next, ahead of every queued command, response, telemetry or heartbeat frame. The tie rule keeps a category’s own frames in the order it sent them
Eight slots, six of them open to ordinary traffic; the rest is refused, not buffered elsewhere Ordinary traffic can never occupy the last two slots. Refusal is visible to the caller, who decides. A category’s send reports failure; a client command reports failure without consuming a sequence number; an acknowledgement is lost only when nothing less urgent is queued
A frame arriving at a full share displaces the newest least urgent queued frame A response or an acknowledgement is never lost to queued heartbeats or telemetry, so a burst of status requests after a restart cannot cost the first command its acknowledgement. Equal priorities never displace each other, so a flood of responses still refuses responses
An emergency frame arriving at a full queue displaces the newest frame of the lowest queued priority The displaced frame’s completion runs exactly once, reporting failure, after the queue is consistent again. So a displaced sender learns what happened, and a completion that sends again behaves as it would anywhere else
A failed emergency frame is sent again at most twice; any other failed frame is reported at once A bus fault cannot trap the queue in unbounded retries, and a transient error does not cost the one frame that must get out
Refusals, displacements, retries and failures are counted A dropped or delayed emergency frame can be observed without a debugger. The counters live on the transport, which the protocol object exposes
Every accepted frame — queued or immediate — raises a send notification This is the hook the protocol layer uses to defer its heartbeat, and it is why a refused frame does not postpone one
The notification has exactly one owner, fixed at construction The protocol object hands it to the transport it constructs. There is no setter, so nothing that reaches the transport through the protocol object can claim or clear it

Server categories have a dedicated emergency send alongside response and telemetry, so a fault report reaches this queue at emergency priority without the category touching the transport directly.

The address stamped into an outbound frame differs by role, and the asymmetry is worth fixing in mind: a server stamps its own address, because a response must say who answered; a client stamps the target’s address, because a command must say who it is for. The same identifier field therefore reads as source on a response and destination on a command. The direction is never ambiguous, because the message type already says which it is.

A node whose address is configured late — from a switch, from stored settings, from a bootloader parameter — can change it after construction. Frames already queued keep the identifier they were built with.

3. Payload composition: bounded, big-endian, sticky

Two small components handle payloads, and between them they make it impossible to write past the end of a frame or read past the end of one.

The sticky part is the design decision. Rather than making every field access return a status the caller must check, the first out-of-range access poisons the whole operation and later ones become no-ops. A caller therefore writes the natural sequence of fields and checks once at the end, and the failure mode is a payload that is never sent or a command that is never acted on — not a half-formed frame or a plausible-looking wrong value.

Two guards complete the picture. The send helpers refuse an invalid composition, so a category cannot transmit a truncated frame even by ignoring the check. And a reader borrows the frame it reads rather than copying it, with binding to a temporary made impossible at compile time, so a reader can never outlive its data.

The consequence a category author must remember is the one thing this layer cannot check: a handler that reads its fields and forgets to check validity will act on zeros. That is catalogued in Chapter 10, §9.

4. Numbers on the wire

Multi-byte fields are big-endian, and floating-point values cross the bus in one of two forms, chosen per field and stated in the message’s specification:

Two properties of the scaled-integer conversion are worth carrying into a design review, because they decide what a peer sees when a value is out of range: