The Built-in Categories: Design Rationale

can-lite ships two categories. Their messages, payload layouts, states and error codes are specified in the Protocol Specification §8 (system) and the Firmware Upgrade Specification (upgrade), and the system category’s role inside the protocol objects is described in Architecture and Design Decisions §6. This chapter answers the questions those documents do not: why each is built the way it is, and what that costs.

1. The system category is a category

Nothing forced the protocol-level conversations — presence, acknowledgement, status, discovery — to travel the same road as application messages. They could have been special-cased inside the protocol objects.

Making them an ordinary category buys three things:

What it costs is one of the eight registration slots, permanently.

Why the halves are not mirror images

The two halves handle disjoint sets of messages, and the asymmetry is informative rather than accidental:

Conversation Server half Client half
Presence handles the client’s broadcast
Acknowledgement sends them recognises them, acts nowhere
Status request handles it
Discovery answers it consumes the answer

A server’s heartbeat arriving at a client finds no handler, and nothing goes wrong: the client already extracted that frame’s value — proof of life — before dispatch (Chapter 7), and clients never acknowledge, so an unhandled message type on the client side is genuinely inert.

The acknowledgement handler on the client half is deliberately empty. The work happens in the protocol object, which runs first because it needs the source identity that dispatch does not carry. The handler exists so that the category’s registered message types remain an accurate description of what it understands — a small piece of hygiene, not dead code.

What the application does and does not see

The server exposes nothing of the system category: everything it does is already reflected in the server’s own interface — online and offline notifications, automatic answers to status and discovery. Exposing it would let an application answer a status request differently from the protocol’s definition.

The client exposes only discovery, because it is the one answer an application must interpret.

Two consequences follow, and both are simplifications rather than oversights:

2. The firmware upgrade category keeps no state

This is the library’s fullest worked example of a category whose state lives in the application. The category owns message framing, one timer and response encoding; the flash layout, the buffering, the checksum and the bank switch belong to the product.

The division is what makes the category reusable across products with completely different storage. It is also what makes the application’s obligations non-negotiable, listed in §4 below.

Why it does not validate sequence numbers

Two reasons, both practical:

  1. The block index already orders the transfer. A duplicated or reordered block is detected by the application against its own expectation and answered with a category error that says which block was expected — which a protocol-level sequence error could not.
  2. A sequence number would cost a payload byte in the message that sends the most bytes. One byte out of seven, on the longest transfer the protocol ever performs, is a sixth of the throughput.

The trade-off is real: this category has no protocol-level replay protection. It is bounded instead by the checksum at the end — a replayed or lost block produces a mismatch and the image is not activated — and by the authenticity check the application is expected to add.

Why polling does not extend the session

The session timer is extended by the commands that make progress and stopped by the ones that end the transfer. Asking for progress does neither.

That is the design decision worth remembering: a client that has crashed mid-transfer, but whose supervisor still polls for progress, must not be able to hold the server’s staging area open indefinitely.

When the timer does expire the category notifies the application and does nothing else — no frame is sent, because there is nobody to tell, and no state is changed, because the state that matters belongs to the application.

The asynchronous completion pattern

Flash operations take milliseconds to seconds, and nothing in can-lite may block for that long. Every notification therefore carries a completion for the application to call when it is done, and the category retains nothing across the gap — everything it needs to compose the answer is captured when it notifies.

Two obligations fall on the application, and neither can be checked:

Why every command produces two frames back

A category response and a protocol acknowledgement answer different questions: the acknowledgement says the command was well-formed and reached a handler; the response says what happened. When a handler fails, the acknowledgement reports a category-level failure and the detail travels in the response — which is exactly the split described in Chapter 5, §2.

The cost is bus traffic: acknowledgements are nearly as expensive as the commands they answer (Chapter 11, §6). A category that carries its own status, as this one does, is the case where switching them off would save a third of the traffic — a change the protocol does not currently allow, and a fair candidate for a future extension.

3. Throughput is bounded by round trips, not by the bus

The transfer is stop-and-wait: each block waits for its acknowledgement before the next is sent. The consequence is that round-trip latency, not bitrate, sets the throughput — making the bus twice as fast barely helps.

That is why all three extensions recorded in the specification — windowed acknowledgement, page addressing, and carrying blocks over segmentation (Chapter 8) — attack the number of round trips rather than the frame time. The arithmetic is in Chapter 11, §4.

4. What the application must provide

Concern What the category expects
Storage layout Somewhere to stage an image that is not the running one
Size policy Refuse an image that does not fit
Concurrency policy Refuse a second session while one is open
Block ordering Compare each index with the expected one and report a gap
Buffering Accumulate small blocks into whatever unit the storage writes
Verification Check the image against the client’s checksum
Activation Switch to the new image, ideally with rollback if it fails to run
Timeout recovery Discard staging state when the session expires
Authenticity Verify a signature, if the product needs one

The last row is the one to read twice. can-lite authenticates nothing: any node that can put frames on the bus can start an upgrade. Where that matters, the image must carry its own signature and the application must check it before reporting success.