isotp: ISO 15765-2 (ISO-TP) transport over CAN
| (require isotp) | package: isotp |
This package implements the ISO 15765-2 network layer — known as ISO-TP or DoCAN — in pure Racket. ISO-TP carries multi-packet messages over CAN bus frames, and is the transport beneath automotive diagnostics protocols such as Unified Diagnostic Services (UDS, ISO 14229) and OBD-II (ISO 15031).
The implementation is a codec plus state machines; it deliberately does not talk to any CAN driver itself:
bytes->isotp-frame and isotp-frame->bytes encode and decode the four protocol data units (SF, FF, CF, FC), including the CAN FD escape encodings.
The sender state machine segments a payload, honors the peer’s flow control (block size, STmin pacing, FC.WAIT) and times out N_Bs.
The receiver state machine reassembles payloads, emits flow control, answers overflow with FC.OVFLW and times out N_Cr.
Because both machines are pure — every event function takes a timestamp and returns frames to transmit — they are trivial to unit-test, deterministic to replay, and independent of the CAN binding (SocketCAN, a Vector bus, an embedded MCU, or a test double). Both classic CAN (8-byte frames) and CAN FD (12–64-byte frames) are supported, as are all seven addressing modes of the standard.
1 Quick start
A sender and a receiver meet over an idealized bus: every due-frame the sender emits is fed to the receiver, and every flow-control frame the receiver emits goes back to the sender. Timestamps are milliseconds on any monotonic clock.
(require isotp) (define cfg (make-isotp-config #:block-size 8 #:stmin-ms 10)) (define-values (tx frames) (start-tx cfg #"hello ISO-TP world, this is long" 0)) (define rx (make-rx-state cfg)) (define-values (rx2 payload fc o) (rx-frame cfg rx (due-frame-bytes (car frames)) 0))
2 Configuration
procedure
(make-isotp-config [ #:addressing addressing #:tx-id tx-id #:rx-id rx-id #:target-address target-address #:source-address source-address #:address-extension address-extension #:physical-id physical-id #:functional-id functional-id #:frame-size frame-size #:block-size block-size #:stmin-ms stmin-ms #:stmin-us stmin-us #:n-bs n-bs #:n-cr n-cr #:wait-limit wait-limit #:max-payload max-payload #:rx-buffer-size rx-buffer-size #:tx-padding tx-padding #:rx-padding rx-padding]) → isotp-config?
addressing :
(or/c 'normal-11 'normal-29 'normal-fixed-29 'extended-11 'extended-29 'mixed-11 'mixed-29) = 'normal-11 tx-id : (or/c #f exact-nonnegative-integer?) = #f rx-id : (or/c #f exact-nonnegative-integer?) = #f target-address : (or/c #f (integer-in 0 255)) = #f source-address : (or/c #f (integer-in 0 255)) = #f address-extension : (or/c #f (integer-in 0 255)) = #f physical-id : (or/c #f exact-nonnegative-integer?) = #f functional-id : (or/c #f exact-nonnegative-integer?) = #f frame-size : (integer-in 8 64) = 8 block-size : (integer-in 0 255) = 8 stmin-ms : (or/c #f (integer-in 0 127)) = #f stmin-us : (or/c #f (integer-in 100 900)) = #f n-bs : real? = 1000 n-cr : real? = 1000 wait-limit : exact-nonnegative-integer? = 0 max-payload : exact-nonnegative-integer? = 4095 rx-buffer-size : exact-nonnegative-integer? = 4095 tx-padding : (or/c #f (integer-in 0 255)) = #f rx-padding : (or/c #f (integer-in 1 64)) = #f
#:addressing — the addressing mode. normal-* spend no payload bytes on addressing; extended-* spend the first payload byte on the target address; normal-fixed-29 and mixed-29 encode addresses in the 29-bit identifier (see Addressing helpers); mixed-* additionally carry an address-extension byte.
#:frame-size — the transmitted CAN_DL: 8 for classic CAN, one of '(12 16 20 24 32 48 64) for CAN FD. config-fd? reports whether the configuration uses CAN FD.
#:block-size and #:stmin-ms/#:stmin-us — the BS and STmin values this node grants as a receiver when it sends FC.CTS. A sender’s pacing comes from the STmin the peer grants. STmin may be given in milliseconds (0–127) or microseconds (100–900, in 100 µs steps).
#:n-bs / #:n-cr — timeouts in milliseconds for waiting for a flow-control frame (N_Bs) and for the next consecutive frame (N_Cr). The standard value is 1000 ms.
#:wait-limit — how many consecutive FC.WAIT frames the sender tolerates. The default 0 rejects FC.WAIT outright; set a positive limit (e.g. 32) when the peer is allowed to stall.
#:max-payload — accepted by start-tx. Classic CAN caps at 4095; CAN FD may go up to (sub1 (expt 2 32)) using the escaped first-frame encoding.
#:rx-buffer-size — first frames declaring more than this are answered with FC.OVFLW.
#:tx-padding — a fill byte; every transmitted frame is padded up to #:frame-size. #:rx-padding — the minimum CAN_DL accepted on reception (frames shorter than this are ignored).
value
isotp-config? : predicate?
procedure
(config-fd? cfg) → boolean?
cfg : isotp-config?
procedure
(config-prefix-size cfg) → (integer-in 0 1)
cfg : isotp-config?
procedure
cfg : isotp-config?
3 Addressing helpers
procedure
(isotp-tx-id cfg [functional?]) → exact-nonnegative-integer?
cfg : isotp-config? functional? : boolean? = #f
procedure
(isotp-rx-id cfg [functional?]) → exact-nonnegative-integer?
cfg : isotp-config? functional? : boolean? = #f
4 Frame codec
struct
(struct isotp-frame ( type data flow-status block-size stmin-us raw-stmin sequence-number escape? can-dl)) type : symbol? data : (or/c #f bytes?) flow-status : (or/c #f symbol?) block-size : (or/c #f (integer-in 0 255)) stmin-us : (or/c #f real?) raw-stmin : (or/c #f (integer-in 0 255)) sequence-number : (or/c #f exact-nonnegative-integer?) escape? : boolean? can-dl : exact-nonnegative-integer?
procedure
(bytes->isotp-frame payload [prefix-size]) → isotp-frame?
payload : bytes? prefix-size : exact-nonnegative-integer? = 0
procedure
(isotp-frame->bytes frame [ #:fill fill #:pad-byte pad-byte]) → bytes? frame : isotp-frame? fill : exact-nonnegative-integer? = 0 pad-byte : (integer-in 0 255) = 0
procedure
(sf-frame data) → isotp-frame?
data : bytes?
procedure
(sf-frame/escape data) → isotp-frame?
data : bytes?
procedure
(ff-frame total data) → isotp-frame?
total : (integer-in 1 4095) data : bytes?
procedure
(ff-frame/escape total data) → isotp-frame?
total : (integer-in 4096 4294967295) data : bytes?
procedure
(cf-frame sn data) → isotp-frame?
sn : (integer-in 0 15) data : bytes?
procedure
(fc-frame flow-status block-size stmin-us) → isotp-frame?
flow-status : (or/c 'cts 'wait 'overflow) block-size : (integer-in 0 255) stmin-us : (integer-in 0 900000)
procedure
b : (integer-in 0 255)
procedure
(stmin-us->byte us) → (integer-in 0 255)
us : (integer-in 0 900000)
5 Sending
procedure
(start-tx cfg payload now) →
tx-state? (listof due-frame?) cfg : isotp-config? payload : bytes? now : real?
procedure
(tx-flow-control cfg st fc now)
→
tx-state? (listof due-frame?) outcome? cfg : isotp-config? st : tx-state? fc : isotp-frame? now : real?
procedure
(tx-tick cfg st now) →
tx-state? (listof due-frame?) outcome? cfg : isotp-config? st : tx-state? now : real?
struct
(struct tx-state ( phase payload offset next-sn block-left next-ready-at fc-wait-start wait-count granted-stmin-us message)) phase : symbol? payload : bytes? offset : real? next-sn : (integer-in 0 15) block-left : real? next-ready-at : real? fc-wait-start : real? wait-count : exact-nonnegative-integer? granted-stmin-us : real? message : string?
6 Receiving
procedure
(make-rx-state cfg) → rx-state?
cfg : isotp-config?
procedure
(rx-frame cfg st payload now) →
rx-state? (or/c #f bytes?) (or/c #f due-frame?) outcome? cfg : isotp-config? st : rx-state? payload : bytes? now : real?
procedure
(rx-tick cfg st now) →
rx-state? (or/c #f bytes?) (or/c #f due-frame?) outcome? cfg : isotp-config? st : rx-state? now : real?
struct
(struct rx-state ( phase total chunks received expected-sn cf-since-fc last-activity message)) phase : symbol? total : (or/c #f exact-nonnegative-integer?) chunks : (listof bytes?) received : real? expected-sn : (integer-in 0 15) cf-since-fc : exact-nonnegative-integer? last-activity : real? message : string?
7 Results and due frames
8 Caveats and roadmap
Timeouts and pacing are driven by the caller’s timestamps; there is no internal thread. Bind the machines to your scheduler or CAN driver loop.
The library does not filter arbitration identifiers — register the ids from isotp-tx-id / isotp-rx-id with your CAN driver and dispatch incoming payloads by identifier.
Roadmap: a convenience event-loop adapter, J1939-style fixed-address table helpers, and property-based fuzzing of the codec.