isotp:   ISO 15765-2 (ISO-TP) transport over CAN
1 Quick start
2 Configuration
make-isotp-config
isotp-config?
config-fd?
config-prefix-size
config-data-capacity
3 Addressing helpers
isotp-tx-id
isotp-rx-id
4 Frame codec
isotp-frame
bytes->isotp-frame
isotp-frame->bytes
sf-frame
sf-frame/  escape
ff-frame
ff-frame/  escape
cf-frame
fc-frame
stmin-byte->us
stmin-us->byte
5 Sending
start-tx
tx-flow-control
tx-tick
tx-state
6 Receiving
make-rx-state
rx-frame
rx-tick
rx-state
7 Results and due frames
outcome
due-frame
8 Caveats and roadmap
9.3

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
Creates a connection configuration. One value describes one ISO-TP connection and is shared by the sender and receiver machines.

  • #: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?

Predicate for configuration values. An accessor is provided for every configuration field (e.g. isotp-config-addressing, isotp-config-frame-size), plus the config-fd?, config-prefix-size and config-data-capacity helpers below.

procedure

(config-fd? cfg) → boolean?

  cfg : isotp-config?
Reports whether the configuration selects CAN FD frame sizes (#:frame-size greater than 8).

procedure

(config-prefix-size cfg) → (integer-in 0 1)

  cfg : isotp-config?
Number of payload bytes spent on addressing (one for extended and mixed modes, zero otherwise).

Payload bytes available per full frame: the frame’s CAN_DL minus the addressing prefix.

3 Addressing helpers🔗ℹ

procedure

(isotp-tx-id cfg [functional?]) → exact-nonnegative-integer?

  cfg : isotp-config?
  functional? : boolean? = #f
The arbitration identifier this node transmits on; functional? selects the 1-to-n identifier. For normal-fixed-29 and mixed-29 this composes the standard-mandated 29-bit layout: physical prefix 416940032 / functional 417005568 (mixed: 416153600 / 416088064), then the target address in bits 15–8 and the source address in bits 7–0. Custom prefixes can be installed with #:physical-id / #:functional-id (bits 28–16).

procedure

(isotp-rx-id cfg [functional?]) → exact-nonnegative-integer?

  cfg : isotp-config?
  functional? : boolean? = #f
The identifier this node receives on (prefix, then source address in bits 15–8 and target address in bits 7–0).

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?
One protocol data unit. type is 'sf, 'ff, 'cf or 'fc. For FC frames, flow-status is 'cts, 'wait or 'overflow, and stmin-us is the decoded separation time. For CF frames sequence-number is the 4-bit sequence number; for FF frames it is the total message length. escape? marks the CAN FD extended-length encodings.

procedure

(bytes->isotp-frame payload [prefix-size]) → isotp-frame?

  payload : bytes?
  prefix-size : exact-nonnegative-integer? = 0
Decodes one CAN payload (everything after the identifier). Any addressing prefix has been counted in prefix-size and is skipped. Trailing padding is accepted; a PCI that claims more bytes than the frame carries raises exn:fail. When the frame’s CAN_DL exceeds 8, a single frame must use the escape encoding.

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
Encodes a frame, padding up to fill bytes with pad-byte (the natural size is used when fill is smaller).

procedure

(sf-frame data) → isotp-frame?

  data : bytes?
Plain single frame (1–7 payload bytes).

procedure

(sf-frame/escape data) → isotp-frame?

  data : bytes?
Escaped single frame (1–62 bytes), mandatory on CAN FD frames with CAN_DL > 8.

procedure

(ff-frame total data) → isotp-frame?

  total : (integer-in 1 4095)
  data : bytes?
First frame with the 12-bit length encoding.

procedure

(ff-frame/escape total data) → isotp-frame?

  total : (integer-in 4096 4294967295)
  data : bytes?
First frame with the 32-bit escape length.

procedure

(cf-frame sn data) → isotp-frame?

  sn : (integer-in 0 15)
  data : bytes?
Consecutive frame with sequence number sn.

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)
Flow-control frame.

procedure

(stmin-byte->us b) → exact-nonnegative-integer?

  b : (integer-in 0 255)
Decodes a wire STmin byte to microseconds. Follows ISO 15765-2 §9.2.4.2: 0x00–0x7F is 0–127 ms, 0xF1–0xF9 is 100–900 µs, and reserved values are treated as 127 ms.

procedure

(stmin-us->byte us) → (integer-in 0 255)

  us : (integer-in 0 900000)
Encodes microseconds to a STmin byte: whole milliseconds up to 127 ms, or 100–900 µs in 100 µs steps; anything else raises exn:fail.

5 Sending🔗ℹ

procedure

(start-tx cfg payload now) → 
tx-state? (listof due-frame?)
  cfg : isotp-config?
  payload : bytes?
  now : real?
Begins transmitting payload. Returns either a single frame (SF) or the first frame (FF) of a multi-frame transfer; in the latter case the state waits for flow control.

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?
Feeds a received flow-control frame to the sender. FC.CTS releases the next block of consecutive frames (paced by the STmin granted in fc); FC.WAIT is tolerated up to #:wait-limit times; FC.OVFLW fails the connection.

procedure

(tx-tick cfg st now) → 
tx-state? (listof due-frame?) outcome?
  cfg : isotp-config?
  st : tx-state?
  now : real?
Advances time: releases consecutive frames whose due-frame-ready-at has passed and raises an N_Bs timeout error when no flow control arrives in time. Call this from a timer.

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?
Sender progress. phase is 'idle, 'wait-fc, 'send-cf, 'done or 'error.

6 Receiving🔗ℹ

procedure

(make-rx-state cfg) → rx-state?

  cfg : isotp-config?
Creates an idle receiver state.

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?
Feeds one received CAN payload (identifier excluded, addressing prefix included) to the receiver. Returns the new state, a completed payload when a message finished arriving, flow-control bytes to transmit when one is due (CTS on first frames and at block boundaries, OVFLW on overflow), and an outcome. A single frame interrupts an ongoing reception; a retransmitted first frame is ignored; a wrong sequence number or an N_Cr timeout fails the connection.

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?
Advances time and detects the N_Cr timeout. Call this from a timer.

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?
Receiver progress. phase is 'idle, 'wait-cf or 'error.

7 Results and due frames🔗ℹ

struct

(struct outcome (status message))

  status : symbol?
  message : string?
Result of a state-machine step: 'running, 'done or 'error, with a human-readable message.

struct

(struct due-frame (bytes ready-at))

  bytes : bytes?
  ready-at : real?
A frame to put on the bus, not before the timestamp ready-at (milliseconds, same clock as the now arguments).

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.