can-lite: Protocol Specification

Version: 1.0
Protocol Version Byte: 1
Status: Draft
Date: 2026

1. Abstract

This document specifies the can-lite CAN bus protocol, a lightweight client-server communication protocol operating over CAN 2.0B (29-bit extended identifiers) at up to 1 Mbit/s. The protocol provides a minimal built-in System category with heartbeat, command acknowledgement, status request, and category discovery messages. Applications extend the protocol by registering custom category handlers on the server.

2. Terminology

Term Definition
Server A node on the CAN bus that listens for commands, processes them, and sends responses. Each server has a unique node ID and serves exactly one client.
Client The initiator of all commands and queries. A single client can communicate with multiple servers.
Broadcast A message addressed to all servers (node ID 0x000)
Category A 4-bit field in the CAN identifier that groups related message types. The built-in System category (0x0) is always available; applications register additional categories.
Category Handler A CanCategoryServer or CanCategoryClient implementation registered via RegisterCategory() that processes all messages for a specific category.
Sequence Number An 8-bit counter in byte[0] of command frames giving in-order delivery and duplicate detection; see §11 for why it is not a security mechanism
Scale Factor Integer multiplier used to convert floats to fixed-point integers

3. Architecture

The protocol follows a client-server model:

4. Transport

4.1 ISO-TP Segmentation (ISO 15765-2)

For payloads exceeding the 8-byte CAN frame limit, can-lite provides an optional ISO-TP transport layer (IsoTpTransportImpl) that implements ISO 15765-2 segmentation and reassembly.

Frame Types

PCI Nibble Frame Type Description
0x0N Single Frame (SF) N = data length (1–7); entire PDU fits in one frame
0x1NNN First Frame (FF) NNN = total PDU length (8–4095); first 6 bytes
0x2N Consecutive Frame N = sequence number 0–F (wraps); up to 7 bytes
0x3S Flow Control (FC) S = status: 0=CTS, 1=Wait, 2=Overflow

Flow Control Fields

Byte Field Description
0 PCI 0x3S — Flow Status (0=CTS, 1=Wait, 2=Overflow; 3–15 are reserved and abort the transfer)
1 BS Block Size — number of CFs before next FC (0 = unlimited)
2 STmin Minimum separation time: 0x00–0x7F = 0–127 ms; 0xF1–0xF9 = 100–900 µs (ISO-TP sub-ms range); 0x80–0xF0 and 0xFA–0xFF = reserved (treated as 0x7F / 127 ms per ISO 15765-2)

Timing Parameters

Parameter Value Description
N_As 1000 ms Sender timeout for a Single Frame or Consecutive Frame to be reported sent (REQ-CAN-047)
N_Bs 1000 ms Sender timeout waiting for a Flow Control frame
N_Cr 1000 ms Receiver timeout waiting for a Consecutive Frame
N_WFTmax 16 Maximum consecutive Flow Control Wait frames before the sender aborts (implementation-defined per ISO 15765-2)

The receiver advertises the block size and separation time it is configured with, through IsoTpTransportImpl::Config (default 0 and 0, meaning no pacing), and sends a further Flow Control frame after each complete block. Every frame of a transfer is charged against the receiving server’s rate limit (§12). A node that receives long transfers alongside other traffic should therefore configure a separation time of at least 1000 / limit milliseconds, so that a peer cannot exceed the limit within one transfer.

On FS = Wait, the sender restarts N_Bs and continues waiting rather than aborting immediately; only N_WFTmax consecutive Wait frames (or an N_Bs timeout) abort the transfer.

A Flow Control frame the sender cannot decode — shorter than three bytes, or carrying a reserved Flow Status — aborts the transfer as an unexpected frame. It is deliberately not reported as an overflow, which is a state only the peer can declare.

N_Bs is armed the moment the sender enters the wait-for-Flow-Control state, before the frame is handed to the driver, so a driver that never reports completion still times out rather than leaving the sender wedged.

Unexpected and Failed Transfers

The receiver handles unexpected frames as ISO 15765-2 requires (REQ-CAN-046):

A transfer that fails, in either direction, is discarded and reported once through the transport’s SetOnAbort callback, which names the identifier the data travelled on and the reason. The channel stays open. The abort callback belongs to the application: neither protocol object installs it, and neither releases channels.

Integration

IsoTpTransportImpl is attached to CanProtocolServer or CanProtocolClient via AttachIsoTpTransport(IsoTpTransport&). When attached, incoming frames are first offered to the ISO-TP layer; if no channel claims the frame, it falls through to normal category dispatch. PDUs reassembled by the transport layer are delivered via the SetOnPduReceived callback.

A channel is a connection with one peer, identified by a receive and a transmit identifier (REQ-CAN-035):

The implementation uses WithStorage for zero-heap construction. All internal components (IsoTpSender, IsoTpReceiver, IsoTpChannelImpl) are non-template classes that receive their PDU buffer storage via infra::WithStorage aliases, following the EMIL convention. An application sizes IsoTpTransportImpl::WithStorage<MaxPduSize, MaxChannels> over the protocol object’s own frame transport, attaches it with AttachIsoTpTransport, and opens each receive channel with RegisterReceiveChannel.

Sending to a category. Sending is the application’s: neither protocol object nor any category calls SendPdu. A PDU addressed to a server category travels on a data identifier built like a command’s (§5): command priority, the category, the message type and the server’s node ID. The receiving protocol object dispatches the reassembled PDU to that category’s segmented handler, through the same admission, sequence and acknowledgement path as a single frame (§11). For a category that validates sequence numbers, the PDU’s first byte is the sequence number. The application reserves it from the client, which is the sequence source; it cancels the reservation if SendPdu refuses the transfer, and returns the number if the transfer is reported through the abort callback. SendPdu’s completion runs only when the transfer succeeds.

Addressing constraint. IsoTpTransportImpl routes frames to channels purely by the literal CAN ID carried in dataId/fcId — the sender’s identity is not otherwise encoded. AttachIsoTpTransport() alone does not register any channel; the application (or a category built on top of it) must explicitly call RegisterReceiveChannel() for each dataId/fcId pair it expects multi-frame traffic on, using IDs that unambiguously identify a single correspondent (e.g. a dedicated node-to-node exchange such as a firmware upload session). A dataId shared by more than one concurrent sender at a time will have their frames interleaved into the same channel; the transport layer does not detect or reject this. Because of this, can-lite does not provide automatic multi-frame support for arbitrary category command/response traffic across many nodes — only the point-to-point case with an explicitly-managed channel is supported.

5. CAN Identifier Layout

All 29 bits of the extended CAN ID are structured as follows:

Bit:  28  27  26  25  24  23  22  21  20  19  18  17  16  15  14  13  12  11  10  9   8   7   6   5   4   3   2   1   0
     |----- Priority ----|--- Category --|--------- Message Type --------|------------------- Node ID -------------------|
     |   5 bits (0-1F)   |  4 bits (0-F) |         8 bits (0-FF)         |                12 bits (0-FFF)                |

Field Encoding:

raw_id = (priority << 24) | (category << 20) | (message_type << 12) | node_id

6. Priority Levels

Value Name Usage
0 Emergency Safety-critical events
4 Command Commands, parameter writes
8 Response Command acknowledgements
12 Telemetry Periodic measurements
16 Heartbeat Node liveness

Lower numerical values have higher CAN bus arbitration priority.

Emergency priority means more than arbitration. A server admits an emergency command outside the ordinary sequence and rate-limit rules (§11, §12), and every node sends queued emergency frames first, from queue capacity kept for them (REQ-CAN-039, REQ-CAN-040). Priority also decides admission to a full send queue: a frame that finds its share taken displaces the newest queued frame of the lowest priority when that frame is less urgent than itself, and is refused otherwise, so a response or an acknowledgement is never lost to queued heartbeats or telemetry (REQ-CAN-040).

7. Message Categories

Value Name Description
0x0 System Heartbeat, command acknowledgement, status request, category discovery
0x1 Firmware Upgrade Block-based firmware transfer, verification, and activation
0x2 - 0xF Application Reserved for application-defined categories

The System category is always available. Category 0x1 is defined in a separate extension specification:

Applications register additional categories (values 0x2-0xF) by providing CanCategoryServer and CanCategoryClient implementations to CanProtocolServer::RegisterCategory() and CanProtocolClient::RegisterCategory(). Both return false, without registering, if the category’s ID is already registered or if 8 categories are already registered (the System category always occupies one of those 8 slots). See Extending can-lite with categories.

7.1 Reserved Message Type

Message type 0xFE is reserved across all categories for a category error response, carrying a category-specific failure detail that the universal acknowledgement status cannot express.

Byte Field Type Description
0 Originating Command uint8 Message type of the command that failed
1 Error Code uint8 Category-defined error code

A category error response is normally followed by a command acknowledgement with status categoryError.

8. Message Catalog

8.1 System (Category 0x0)

8.1.1 Heartbeat (Type 0x01)

Sent at CanPriority::heartbeat. No sequence validation.

Byte Field Type Description
0 Version uint8 Protocol version (currently 1)

Both the server and the client automatically transmit heartbeat messages starting from the moment they are constructed. A heartbeat is sent at least 1 second after the last transmitted message of any kind, by whichever side is sending it. This ensures liveness detection without adding traffic when that side is already actively communicating. The heartbeat acts as a keep-alive: it fires only during periods of silence. The server keeps at most one heartbeat queued or in transmission at a time: while one is pending, neither the timer nor a status request adds another, and the pending one answers them (REQ-CAN-048). The client’s heartbeat is broadcast (node ID 0x000), since the client has no node ID of its own on the bus.

The client uses received heartbeats to track server liveness. When a heartbeat (or any message) is received from a server, the client considers that server online and resets that server’s timeout timer. If no messages are received from a server within a configurable timeout (default 3 s), the client considers it offline and notifies the application via CanProtocolClientObserver::OnServerOffline(nodeId). Multiple servers (up to 8) can be tracked simultaneously with independent liveness timers.

Symmetrically, the server uses received traffic to track whether its client is still present. Every command the server accepts (re)starts its client timeout timer (default 3 s, Config::clientTimeout). A command is accepted when it is addressed to the server, belongs to a registered category, is a known command type, passes sequence validation where the category requires it (§11), and has a payload its handler accepts. A client heartbeat is an accepted command like any other. A frame the server rate-limits, discards or answers with an error does not restart the timer, so invalid traffic cannot keep a departed client online. The first accepted command after start-up, or after the client went offline, notifies CanProtocolServerObserver::Online(); later ones do not. If the timer expires without a further accepted command, the server notifies CanProtocolServerObserver::Offline(). The two notifications therefore alternate, one per transition.

A server tracks only one client, per REQ-CAN-006.1. The identifier carries the destination node but not the source, so the server cannot tell controllers apart. It assumes a single trusted controller on the bus and treats any accepted command as proof that this controller is present. A bus with several controllers would need a source or session identity in the frame, which this protocol does not define.

8.1.2 Command Acknowledgement (Type 0x02)

Sent by the server at CanPriority::response.

Byte Field Type Description
0 Category uint8 CanCategory of the acknowledged command
1 Command uint8 CanMessageType of the acknowledged command
2 Status uint8 See acknowledgement status table
3 Expected Sequence uint8 Meaningful only when Status is Sequence Error (4); 0 otherwise

The category byte ensures the client can uniquely identify which command is being acknowledged, since message type values may be reused across categories.

On receiving a sequenceError acknowledgement, the client adopts byte 3 as its next sequence number for that server, so that a single lost frame, or the client’s own sequence state having reset (for example after a restart), does not leave the two sides permanently out of sync — see §11.

If no acknowledgement of any status arrives for a sequence-validated command within a configurable timeout (default 1 s, Config::commandAckTimeout), the client notifies the application via CanProtocolClientObserver::OnCommandAckTimeout(nodeId, category, messageType). The command itself is not retried; the application decides whether and how to retry.

8.1.3 Status Request (Type 0x03)

Sent by the client at CanPriority::command. No sequence validation. Empty payload.

When received, the server responds by transmitting a heartbeat message. This allows the client to quickly confirm the server is online without waiting for the next scheduled heartbeat. One heartbeat answers every status request received while it is still waiting to be sent, so a burst of requests, such as the backlog a bus adapter delivers when a server comes back after a restart, produces one heartbeat rather than one per request and cannot fill the server’s send queue ahead of a command’s response and acknowledgement (REQ-CAN-048).

8.1.4 Category List Request (Type 0x04)

Sent by the client at CanPriority::command. No sequence validation. Empty payload.

When received, the server responds with a Category List Response message containing the IDs of all registered category handlers.

The client keeps one discovery pending at a time, addressed to one server; a request to the broadcast address, or while another is pending, is refused. Only a Category List Response from the server asked completes it. If none arrives within a configurable timeout (default 1 s, Config::discoveryTimeout), the discovery completes without an answer.

8.1.5 Category List Response (Type 0x05)

Sent by the server at CanPriority::response.

Byte Field Type Description
0 Category 0 uint8 First registered category ID
1 Category 1 uint8 Second registered category ID
… … uint8 Additional category IDs (up to 8 max)

Each byte contains the ID of one registered category handler. The System category (0x0) is always included. Categories are listed in registration order. The response is limited to 8 category IDs by the CAN frame payload size. RegisterCategory() enforces this bound at registration time: the 9th and any subsequent registration attempt returns false and the category is not registered, so the response can never truncate.

9. Data Encoding

All multi-byte values are encoded big-endian (network byte order). Each message’s specification states, per floating-point field, which of the two encodings below it uses.

9.1 Encoding Algorithm

fixed_value = clamp(round(float_value × scale_factor), INT_MIN, INT_MAX)
float_value = fixed_value / scale_factor

Values are saturated (clamped) to the target integer range to prevent overflow.

9.2 Single-Precision Floating Point

A field carried without a scale factor occupies 4 bytes and holds the IEEE 754 binary32 bit pattern of the value, encoded as a big-endian uint32. The bytes are identical to those of a uint32 field holding the same bit pattern. No rounding, clamping or substitution is applied: signed zero, subnormals, infinities and NaN payloads arrive bit for bit as they were sent, and a receiver that requires a finite value checks for it itself.

Value Bit pattern Byte[0..3]
1.5 0x3FC00000 0x3F 0xC0 0x00 0x00
-2.5 0xC0200000 0xC0 0x20 0x00 0x00
+inf 0x7F800000 0x7F 0x80 0x00 0x00

10. Enumeration Tables

10.1 Acknowledgement Status

Value Status Description
0 Success Command accepted and processed
1 Unknown Command Message type not recognized for category
2 Invalid Payload Payload too short, or rejected by the category’s handler
3 Invalid State Command not valid in current state
4 Sequence Error Sequence number not (previous + 1) mod 256
5 Rate Limited Message rate limit exceeded
6 Not Implemented Command recognized but handler not implemented
7 Category Error Category-specific rejection; details in category 0xFE frame

11. Sequence Number Protocol

12. Rate Limiting

The server enforces a configurable maximum number of received messages per second (default: 500). Messages received after the limit is reached are silently discarded. The rate counter resets automatically every second via an internal timer.

Frames at emergency priority are charged to a separate budget (default: 20 per second, Config::maxEmergencyMessagesPerSecond), reset by the same timer. Ordinary traffic cannot use up the emergency budget, and emergency traffic does not use the ordinary one. So an emergency stop is still admitted when a flood has exhausted the ordinary limit. Rate limiting is the first check after addressing, so a flood of any kind costs only a counter comparison per frame. The server counts every refusal and every admitted emergency command, and exposes these counts through CanProtocolServer::Statistics() (REQ-CAN-044).

Every accepted frame is charged, including one an ISO-TP channel consumes, so a multi-frame PDU costs one credit per frame it occupies on the bus rather than one credit for the whole transfer. The reassembled PDU is not charged again when it is dispatched.

13. Node Addressing

14. Typical Command Flow

15. Extensibility

Applications extend can-lite by defining additional categories:

  1. Define an enum value for the new category (0x2–0xF; 0x1 is reserved for Firmware Upgrade).
  2. Implement CanCategoryServer and CanCategoryClient subclasses that handle the message types within that category.
  3. Register the handler with the server at construction time.

The server dispatches incoming frames to the matching category handler. Frames with unregistered categories are silently discarded. Frames with a registered category but an unknown message type receive an unknownCommand acknowledgement.

16. Security Considerations