Skip to content

title: CanKit.Pro.J1939Tp description: SAE J1939-21 transport protocol: TP.BAM and TP.CM


L3 · Transport SAE J1939-21 transport protocol: TP.BAM and TP.CM

CanKit.Pro.J1939Tp

SAE J1939-21 Transport Protocol (TP) for CanKit.Pro. Implements both flavors of the J1939-21 §5.10 transport service:

Status: 1.3.0 is the first stable release: from 1.3.0 on the public API follows SemVer, so a breaking change costs a major version. 1.0.0 – 1.2.3 were published as stable before the API had been reviewed against the specifications; they are unlisted and deprecated on nuget.org and should not be used. See Versioning.

What is validated, and what is not

Validated: TP.BAM and TP.CM sessions, the T1–T4 timers, Connection Abort codes and retransmission, by the test suite in tests/CanKit.Pro.Tests, between instances of this implementation over CanKit.Adapter.Virtual, directly or through a controllable bus double.

Not validated: Interoperation with third-party J1939 nodes and J1939-21 conformance testing. Nothing in this package has run against real CAN hardware, a conformance tester or a third-party implementation: the test project references CanKit.Adapter.Virtual and no hardware adapter.

  • TP.BAM (Broadcast Announce Message) — one sender pushes an up-to-1785-byte PDU to every node on the bus, no acknowledgement (FR-TP-030).
  • TP.CM (Connection Mode: RTS / CTS / EndOfMsgAck / Connection Abort) — point-to-point, with block-size negotiation and end-of-message acknowledgement (FR-TP-031).

TP.DT (Data Transfer) frames carry the segmented payload for both flavors, sequence-numbered from 1 (FR-TP-033). Every session runs on its own actor-owned state and its own set of IDeadlines (T1, T2, T3, T4 — FR-TP-032), so sessions with different peers — TP.CM to and from several nodes, alongside a BAM — execute in parallel over the same physical bus (FR-TP-034/035) without interfering with each other; sends to one destination take turns, as described below.

The timers carry J1939-21 §5.10.2.4's meanings: T1 (750 ms) between TP.DTs at the receiver, T2 (1250 ms) from the receiver's CTS to the first TP.DT of the block, T3 (1250 ms) at the originator for the response it is owed — CTS after RTS, the next CTS after a block, EndOfMsgAck after the last packet — and T4 (1050 ms) after a CTS(0) hold. Tr (200 ms) is the time a node has to send a response, not a timer a peer is held to, so there is no option for it (#31); Th (500 ms), the holding time between two CTS(0) messages, is not used by this stack, which sends no CTS(0), so there is no option for it either — the BAM inter-packet spacing of §5.10.3 is BamPacketSpacing, named Th before #144. Connection Abort carries table 7's reason codes and nothing outside the table (J1939TpAbortReason), so a peer stack reads the abort as what happened (#33). With SendConnectionAbortRole set it also carries the sender's Connection Abort Role (second edition, byte 3 bits 2–1: 00 originator, 01 responder), which a peer running a CM transfer of the same PGN in both directions needs to tell which of the two one abort ends; the option is off by default, so byte 3 stays 0xFF as in the first edition and in the Linux kernel's j1939 stack, and a strict peer does not refuse the abort over its reserved byte. Whatever the option, an abort received is applied to the direction its PGN names, and where that is both, the role decides — without one (0xFF) both end (#350). A CTS that asks for a packet already sent is served, up to MaxRetransmitRequests (default 2) times per session, after which reason 5 goes out; a PDU1 PGN in a TP.CM is read with its low byte cleared, so a stack that writes the destination address there still reaches the session; and an RTS allowing no packet per CTS opens no session (#58).

ReceiveAllAsync ends at the first reassembly abort it meets — a bad sequence number, T1, a peer's Connection Abort, a BAM superseded by the next one from the same source — by throwing that J1939TpAbortException. The abort ends one peer's transfer, not the channel: what arrives after it is kept, and a caller that wants to keep receiving catches the exception and calls ReceiveAllAsync again, as the J1939 node does (#195). That is the documented behaviour, not an iterator that survives the abort (#356).

The receiver is lenient in two places, by decision (#356). It takes a TP.DT that carries the next expected sequence number even when it lies beyond the packets its last CTS granted, and so never sends table 7's code 6 (unexpected data transfer packet); and an originator that receives a CTS while a block is still draining keeps it and applies it when the block ends, rather than aborting with code 4 (CTS received during data transfer) — a receiver's next CTS can legitimately overtake the originator's own confirmation of the block's last TP.DT. Both codes are recognised when a peer sends them (J1939TpAbortReason); this stack sends neither.

A TP.CM or TP.DT from source address 0xFE (null) or 0xFF (global) is not filtered: a BAM from either is reassembled, and an RTS from either opens a session whose CTS goes to that address. Whether SAE J1939-81's rules for those two addresses mean such frames should be dropped is an open question — the review behind #356 cited them from memory, and the text has not been checked — so the behaviour stays as it is until it is.

Sends are serialized per destination: a send whose destination is busy waits behind the one on the wire, and at most MaxQueuedSendsPerDestination (default 8, counting sends the channel has accepted but not yet started; every BAM goes to the global address, so this bounds the waiting BAMs too) may wait per destination. One more faults at the call, before anything is queued, with J1939TpSendRejectedException instead of growing memory and latency against a peer that answers slowly or never (#204). A queued send has no deadline of its own — a peer may legitimately hold the session ahead of it with CTS(0) — so cancel its token to bound the wait; a cancelled send leaves the queue at once.

Cancelling an active CM send completes its task on the actor without waiting for the Connection Abort's service hand-off. The destination remains reserved until that hand-off, so the next send's RTS is handed to the bus service after the Abort. If the original RTS is still unconfirmed and unanswered, the Abort first waits for it to settle.

The channel ships on nuget.org alongside the other CanKit.Pro.* L2/L3 building blocks. It re-uses:

  • CanKit.Pro.RawCan — one ICanBusService per channel to demultiplex the TP.CM / TP.DT frames back out of the shared bus stream and to confirm outbound frames.
  • CanKit.Pro.Actor — one IProtocolActor mailbox for single-writer session state.
  • CanKit.Pro.Reliability — IDeadlineScheduler for T1/T2/T3/T4 and the BAM packet spacing, cancelled/re-armed on the actor's loop.
  • CanKit.Pro.Addressing — J1939Id / J1939Pgn for composing the TP.CM (PGN 0xEC00) and TP.DT (PGN 0xEB00) 29-bit IDs.

Basic usage

using CanKit.Abstractions.API.Can;
using CanKit.Core;
using CanKit.Pro.J1939Tp;

// One bus, two nodes on their own source addresses.
using var bus = CanBus.Open("virtual://demo/0",
    cfg => cfg.SetProtocolMode(CanProtocolMode.Can20).Baud(500_000));

using var sender   = J1939Tp.Open(bus, sourceAddress: 0x01);
using var receiver = J1939Tp.Open(bus, sourceAddress: 0x02);

// TP.BAM: broadcast to every node.
await sender.SendBamAsync(pgn: 0xFECA, payload: new byte[100]);

// TP.CM: point-to-point with RTS/CTS/EOM.
await sender.SendCmAsync(pgn: 0xFECB, destinationAddress: 0x02, payload: new byte[300]);

var received = await receiver.ReceiveAsync();
// received.Pgn / received.SourceAddress / received.DestinationAddress / received.Payload

Coverage

Requirement Status
FR-TP-030 (TP.BAM send/receive) Yes
FR-TP-031 (TP.CM RTS/CTS/EndOfMsgAck) Yes
FR-TP-032 (T1/T2/T3/T4 via DeadlineScheduler, Connection Abort) Yes
FR-TP-033 (BAM and CM told apart by PGN, TP.DT reassembly, SN 1..N) Yes
FR-TP-034 (parallel sessions on shared bus) Yes
FR-TP-035 (multiple concurrent TP.CM peers) Yes

Install

dotnet add package CanKit.Pro.J1939Tp

# plus a CanKit adapter for the hardware you actually talk to, e.g.
dotnet add package CanKit.Adapter.Virtual   # loopback, no hardware

Dependencies: CanKit.Abstractions, CanKit.Pro.Actor, CanKit.Pro.Addressing, CanKit.Pro.RawCan, CanKit.Pro.Reliability.

Part of CanKit.Pro — higher CAN protocol layers built on top of CanKit, which is consumed as a NuGet package rather than forked.

License

MIT — see LICENSE. CanKit itself is a separate project licensed under Apache-2.0; see THIRD-PARTY-NOTICES.md.