Skip to content

J1939 Transport Protocols, Diagnostics, and DM Messages

Document ID: J19-02 Series: Telematics Tutorial Series Target audience: Advanced — you should understand J1939 addressing, Parameter Group Numbers (PGN), and Suspect Parameter Numbers (SPN) (J19-01).

Cross-references: - For J1939 fundamentals (PGN, SPN, NAME), see J19-01. - For J1939 CAN FD extensions, see J19-03. - For CAN frame format, see CAN-02. - For CAN error handling, see CAN-03.


Learning Objectives

By the end of this document, you will be able to:

  • Describe the Broadcast Announce Message (BAM) transport protocol and trace a complete BAM transfer
  • Describe the Connection Mode Data Transfer (CMDT) transport protocol with Request To Send (RTS) / Clear To Send (CTS) flow control
  • Identify when to use Extended Transport Protocol (ETP) for messages exceeding 1,785 bytes
  • Decode DM1 (Active Diagnostic Trouble Codes) messages including lamp status, SPN, Failure Mode Identifier (FMI), Occurrence Count (OC), and SPN Conversion Method (CM)
  • Describe the purpose of DM2 through DM13 and their role in the J1939 diagnostic framework
  • Implement timeout and error handling for transport protocol sessions

Acronym Reference

Acronym Expansion
ACK Acknowledgment
BAM Broadcast Announce Message
CAN Controller Area Network
CM (transport) Connection Management (TP.CM)
CM (DTC field) SPN Conversion Method (1-bit field in a DTC)
CMDT Connection Mode Data Transfer
CTS Clear To Send
DA Destination Address
DM Diagnostic Message (DM1–DM13+)
DT Data Transfer (TP.DT)
DTC Diagnostic Trouble Code
ECU Electronic Control Unit
ETP Extended Transport Protocol
FMI Failure Mode Identifier
ISO International Organization for Standardization
J1939 SAE J1939 — heavy-duty vehicle network standard
NRC Negative Response Code (used in UDS/J1939-73 diagnostic services)
OC Occurrence Count
PGN Parameter Group Number
RTS Request To Send
SA Source Address
SAE Society of Automotive Engineers
SPN Suspect Parameter Number
TP Transport Protocol

1. Why Transport Protocols?

A single Controller Area Network (CAN) frame carries at most 8 data bytes. Many J1939 messages — Diagnostic Trouble Code (DTC) lists, device configuration, software downloads — require more than 8 bytes. J1939 defines two transport protocols to segment and reassemble large messages:

  • BAM — for broadcast messages (one-to-many, no flow control)
  • CMDT — for connection-oriented transfers (one-to-one, with flow control)

Both protocols use two PGNs: - TP.CM (PGN 60416 / 0xEC00) — Transport Protocol Connection Management - TP.DT (PGN 60160 / 0xEB00) — Transport Protocol Data Transfer


2. BAM (Broadcast Announce Message)

BAM is used when a node needs to broadcast a multi-packet message to all nodes on the network. There is no flow control — the sender announces the transfer and then transmits all data packets at a fixed rate.

2.1 BAM Sequence

Step 1: Sender broadcasts TP.CM_BAM (PGN 0xEC00)
  ┌────┬─────────┬─────┬──────┬──────┬─────┬─────┬──────┐
  │ 32 │ Size_Lo │Size_Hi│ Pkts│ 0xFF│PGN_Lo│PGN_Mid│PGN_Hi│
  │    │ (LSB)   │(MSB)  │     │     │      │       │      │
  └────┴─────────┴───────┴─────┴─────┴──────┴───────┴──────┘
  Byte 1: 0x20 (32) = BAM control byte
  Bytes 2-3: Total message size in bytes (little-endian)
  Byte 4: Number of data packets to follow
  Byte 5: 0xFF (reserved)
  Bytes 6-8: PGN being transported (little-endian, 3 bytes)

Step 2: Sender broadcasts TP.DT packets (PGN 0xEB00)
  ┌──────┬────┬────┬────┬────┬────┬────┬────┐
  │ Seq# │ D1 │ D2 │ D3 │ D4 │ D5 │ D6 │ D7 │
  └──────┴────┴────┴────┴────┴────┴────┴────┘
  Byte 1: Sequence number (1, 2, 3, ...)
  Bytes 2-8: 7 data bytes per packet

  Packets sent at 50–200 ms intervals (minimum 50 ms between packets)
  The BAM sender must wait 50–200 ms between consecutive TP.DT packets
  (J1939-21 §5.10.2.2). The minimum 50 ms ensures slow receivers can keep
  up; the maximum 200 ms prevents the receiver's reassembly timeout from
  expiring. If a TP.DT packet is not received within 750 ms, the receiver
  aborts the transfer.

Step 3: Receivers reassemble data from all TP.DT packets
sequenceDiagram
    participant S as Sender (ECU)
    participant RA as Receiver A
    participant RB as Receiver B

    Note over S: Multi-packet message
exceeds 8 bytes S ->> RA: TP.CM_BAM (PGN 0xEC00) S ->> RB: TP.CM_BAM (PGN 0xEC00) Note right of S: Byte 1: 0x20 (BAM)
Bytes 2-3: Total size
Byte 4: Packet count
Byte 5: 0xFF
Bytes 6-8: PGN rect rgb(230, 245, 255) Note over S,RB: 50-200 ms between each TP.DT packet S ->> RA: TP.DT Seq #1 (PGN 0xEB00) S ->> RB: TP.DT Seq #1 (PGN 0xEB00) Note right of S: Byte 1: Seq# = 1
Bytes 2-8: Data bytes 1-7 S ->> RA: TP.DT Seq #2 (PGN 0xEB00) S ->> RB: TP.DT Seq #2 (PGN 0xEB00) Note right of S: Byte 1: Seq# = 2
Bytes 2-8: Data bytes 8-14 S ->> RA: TP.DT Seq #N (last) S ->> RB: TP.DT Seq #N (last) Note right of S: Last packet padded
with 0xFF to fill 7 bytes end Note over RA: Reassemble all
TP.DT data Note over RB: Reassemble all
TP.DT data rect rgb(255, 235, 235) Note over S,RB: Failure: missed packet (no ACK mechanism) S --x RB: TP.DT Seq #2 (lost) Note over RB: Silently drops entire
transfer — no retry end Note over RA,RB: Timeout T1 = 750 ms:
if no TP.DT within 750 ms,
receiver drops transfer

Figure: J1902 01 bam transport

2.2 BAM Timing

Parameter Value Description
Minimum inter-packet delay 50 ms Minimum time between consecutive TP.DT packets
Maximum inter-packet delay 200 ms Maximum time before receiver assumes transfer failed
Timeout (T1) 750 ms Receiver timeout waiting for next TP.DT packet

2.3 BAM Example: DM1 with 3 DTCs

A DM1 message (Active DTCs) with 3 DTCs is 14 bytes (2 bytes lamp status + 4 bytes per DTC × 3). This exceeds 8 bytes, so BAM is used:

# BAM announcement
bam_cm = bytes([
    0x20,       # Control byte: BAM
    0x0E, 0x00, # Total size: 14 bytes (little-endian)
    0x02,       # Number of packets: ceil(14/7) = 2
    0xFF,       # Reserved
    0xCA, 0xFE, 0x00  # PGN: 0x00FECA = 65226 (DM1)
])

# DM1 payload (14 bytes):
#   Bytes 1-2:  Lamp status (0x04, 0xFF)
#   Bytes 3-6:  DTC 1 — SPN 1436, FMI 17, OC 0, CM 0
#   Bytes 7-10: DTC 2 — SPN 1436, FMI 19, OC 1, CM 0
#   Bytes 11-14: DTC 3 — SPN 239, FMI 4, OC 2, CM 0

# Data packet 1 (sequence number 1): payload bytes 1-7
dt_1 = bytes([
    0x01,                   # Sequence number
    0x04, 0xFF,             # Lamp status bytes
    0x9C, 0x05, 0x11, 0x00, # DTC 1: SPN 1436, FMI 17, OC 0, CM 0
    0x9C,                   # DTC 2 byte 1 (SPN bits 7-0)
])

# Data packet 2 (sequence number 2): payload bytes 8-14
dt_2 = bytes([
    0x02,                   # Sequence number
    0x05, 0x13, 0x01,       # DTC 2 bytes 2-4: SPN 1436, FMI 19, OC 1, CM 0
    0xEF, 0x00, 0x04, 0x02, # DTC 3: SPN 239, FMI 4, OC 2, CM 0
])
# Note: If the last packet has fewer than 7 data bytes, pad with 0xFF

⚠️ Warning: BAM has no acknowledgment mechanism. If a receiver misses a packet (due to bus congestion or a brief disconnection), it silently drops the entire transfer. For reliable point-to-point transfers, use CMDT instead.

BAM — Reusable Python Functions

The following functions encapsulate BAM announcement construction, data packet building, sending, and reassembly into reusable components suitable for integration into larger J1939 applications.

import struct
import time
import can

def build_bam_announcement(pgn: int, data: bytes, source_address: int = 0x00) -> can.Message:
    """Build a BAM (TP.CM_BAM) announcement message.

    Args:
        pgn: The PGN being transported (e.g., 0xFECA for DM1)
        data: The complete multi-packet payload (>8 bytes)
        source_address: Sender's source address

    Returns:
        CAN message for the BAM announcement
    """
    total_size = len(data)
    num_packets = (total_size + 6) // 7  # 7 data bytes per TP.DT

    # TP.CM_BAM: byte 1 = 0x20 (BAM), bytes 2-3 = total size,
    # byte 4 = num packets, byte 5 = 0xFF (reserved),
    # bytes 6-8 = PGN (little-endian, 3 bytes)
    cm_data = bytes([
        0x20,                           # BAM control byte
        total_size & 0xFF,              # Total size LSB
        (total_size >> 8) & 0xFF,       # Total size MSB
        num_packets,                     # Number of packets
        0xFF,                           # Reserved
        pgn & 0xFF,                     # PGN byte 1 (LSB)
        (pgn >> 8) & 0xFF,             # PGN byte 2
        (pgn >> 16) & 0xFF,            # PGN byte 3 (MSB)
    ])

    # TP.CM PGN = 0x00EC00 + destination (0xFF for BAM = broadcast)
    arb_id = 0x1CECFF00 | source_address

    return can.Message(arbitration_id=arb_id, data=cm_data, is_extended_id=True)

def build_bam_data_packets(data: bytes, source_address: int = 0x00) -> list[can.Message]:
    """Build TP.DT data packets for a BAM transfer.

    Args:
        data: The complete multi-packet payload
        source_address: Sender's source address

    Returns:
        List of CAN messages for the data transfer
    """
    packets = []
    num_packets = (len(data) + 6) // 7

    for seq in range(1, num_packets + 1):
        start = (seq - 1) * 7
        chunk = data[start:start + 7]

        # Pad last packet with 0xFF
        if len(chunk) < 7:
            chunk = chunk + bytes([0xFF] * (7 - len(chunk)))

        dt_data = bytes([seq]) + chunk

        # TP.DT PGN = 0x00EB00 + destination (0xFF for BAM)
        arb_id = 0x1CEBFF00 | source_address
        packets.append(can.Message(arbitration_id=arb_id, data=dt_data, is_extended_id=True))

    return packets

def send_bam(bus: can.Bus, pgn: int, data: bytes, source_address: int = 0x00):
    """Send a complete BAM transfer (announcement + data packets).

    Args:
        bus: python-can bus instance
        pgn: PGN being transported
        data: Complete payload (must be >8 bytes)
        source_address: Sender's source address
    """
    # Send BAM announcement
    announcement = build_bam_announcement(pgn, data, source_address)
    bus.send(announcement)

    # Send data packets with 50-200 ms spacing (using 100 ms)
    for packet in build_bam_data_packets(data, source_address):
        time.sleep(0.100)  # 100 ms inter-packet delay
        bus.send(packet)

def reassemble_bam(packets: list[tuple[int, bytes]]) -> bytes:
    """Reassemble a BAM transfer from received TP.DT packets.

    Args:
        packets: List of (sequence_number, data_bytes) tuples

    Returns:
        Reassembled payload (without sequence numbers or padding)
    """
    # Sort by sequence number
    sorted_packets = sorted(packets, key=lambda p: p[0])

    # Verify sequence continuity
    for i, (seq, _) in enumerate(sorted_packets):
        if seq != i + 1:
            raise ValueError(f"Missing packet: expected seq {i+1}, got {seq}")

    # Concatenate data (7 bytes per packet, strip sequence number)
    return b''.join(data for _, data in sorted_packets)

# === Example: Send and Receive DM1 via BAM ===
# Construct a DM1 payload with 3 DTCs (> 8 bytes, requires BAM)
dm1_payload = bytes([
    0x00, 0x00,     # Lamp status: all off
    # DTC 1: SPN 100 (oil pressure), FMI 1, OC 3
    0x64, 0x00, 0x21, 0x03,
    # DTC 2: SPN 110 (coolant temp), FMI 0, OC 1
    0x6E, 0x00, 0x00, 0x01,
    # DTC 3: SPN 190 (engine speed), FMI 2, OC 7
    0xBE, 0x00, 0x42, 0x07,
])

print(f"DM1 payload: {len(dm1_payload)} bytes")
print(f"Requires BAM: {len(dm1_payload) > 8}")
print(f"Packets needed: {(len(dm1_payload) + 6) // 7}")

# On a real bus:
# bus = can.Bus(channel='can0', interface='socketcan')
# send_bam(bus, pgn=0xFECA, data=dm1_payload, source_address=0x00)

3. CMDT (Connection Mode Data Transfer)

CMDT provides reliable, flow-controlled point-to-point transfer. The receiver controls the pace through CTS (Clear To Send) messages.

3.1 CMDT Sequence

Sender                              Receiver
sequenceDiagram
    participant S as Sender
    participant R as Receiver

    Note over S: Has multi-packet message
for specific destination S ->> R: TP.CM_RTS (0x10) Note right of S: Byte 1: 0x10 (RTS)
Bytes 2-3: Total size
Byte 4: Total packets
Byte 5: Max per CTS
Bytes 6-8: PGN Note left of R: T1 = 750 ms
(wait for CTS) R ->> S: TP.CM_CTS (0x11) Note left of R: Byte 1: 0x11 (CTS)
Byte 2: Packets to send (2)
Byte 3: Next seq# (1)
Bytes 4-5: 0xFF
Bytes 6-8: PGN rect rgb(230, 245, 255) Note over S,R: Block 1 — packets permitted by CTS S ->> R: TP.DT Seq #1 Note left of R: T3 = 1250 ms
(between DT packets) S ->> R: TP.DT Seq #2 end Note left of R: T4 = 1050 ms
(wait for CTS/EOM) R ->> S: TP.CM_CTS (0x11) — next block Note left of R: Byte 2: Packets (1)
Byte 3: Next seq# (3) rect rgb(230, 245, 255) Note over S,R: Block 2 S ->> R: TP.DT Seq #3 (last) Note right of S: Last packet padded
with 0xFF end R ->> S: TP.CM_EndOfMsgAck (0x13) Note left of R: Confirms complete
reception rect rgb(255, 255, 230) Note over S,R: Alt: CTS Hold (flow control pause) R -->> S: CTS (0x11), count = 0 Note over S: Sender waits —
must not transmit
until non-zero CTS R ->> S: CTS (0x11), count > 0 Note over S: Resume sending end rect rgb(255, 235, 235) Note over S,R: Alt: Connection Abort (either direction) S --x R: Abort (0xFF) Note right of S: Byte 1: 0xFF
Byte 2: Reason (1-5)
1 = Already in session
2 = Insufficient resources
3 = Timeout
4 = CTS during transfer
5 = Max retransmit limit
Bytes 3-5: 0xFF
Bytes 6-8: PGN end

Figure: J1902 02 cmdt transport

📝 Note: The sequence diagram and the table below describe the same CMDT flow from different perspectives — the diagram shows temporal ordering, while the table details the byte-level contents of each message.

3.2 CMDT Control Bytes

Control byte Value Message type Direction
0x10 16 RTS (Request To Send) Sender → Receiver
0x11 17 CTS (Clear To Send) Receiver → Sender
0x13 19 EndOfMsgAck Receiver → Sender
0xFF 255 Connection Abort Either direction

3.3 RTS Frame Format

Byte 1: 0x10 (RTS)
Bytes 2-3: Total message size (little-endian)
Byte 4: Total number of packets
Byte 5: Maximum packets per CTS (sender's limit)
Bytes 6-8: PGN being transported (little-endian)

3.4 CTS Frame Format

Byte 1: 0x11 (CTS)
Byte 2: Number of packets receiver is ready to accept
Byte 3: Next expected sequence number
Bytes 4-5: 0xFF, 0xFF (reserved)
Bytes 6-8: PGN being transported

CTS Hold Semantics

A CTS with a packet count of 0 and a next-packet-number of 0xFF is a “hold” message — it tells the sender to pause transmission without aborting the connection. The receiver sends a hold when it needs time to process received data. This is a non-normative convention widely implemented in practice but not explicitly required by J1939-21.

CTS Hold message:
  Byte 1: 0x11 (CTS)
  Byte 2: 0x00 (zero packets — hold)
  Byte 3: 0xFF (no next packet)
  Bytes 4-5: 0xFF, 0xFF (reserved)
  Bytes 6-8: PGN being transported

Non-normative: The CTS hold mechanism is widely implemented but is a non-normative convention — not all J1939 stacks support it. When interoperating with unknown devices, do not assume hold support. Your implementation should include a timeout for hold states and fall back to aborting the connection if no follow-up CTS is received within a reasonable period (e.g., 3 consecutive hold messages or a total hold duration exceeding T1).

3.5 Connection Abort

Either party can abort a CMDT transfer:

Byte 1: 0xFF (Abort)
Byte 2: Abort reason
Bytes 3-5: 0xFF, 0xFF, 0xFF (reserved)
Bytes 6-8: PGN being transported
Abort reason Description
1 Already in a session for this PGN, cannot support another
2 Insufficient system resources
3 Timeout
4 CTS message received when data transfer is in progress
5 Maximum retransmit request limit reached

3.6 CMDT Timing

Parameter Timeout Description
T1 (RTS → CTS) 750 ms Sender waits for CTS after sending RTS
T2 (CTS → DT) 1250 ms Receiver waits for first DT after sending CTS
T3 (DT → DT) 1250 ms Receiver waits between consecutive DT packets
T4 (DT → CTS/EndOfMsgAck) 1050 ms Sender waits for next CTS or EndOfMsgAck

4. Extended Transport Protocol (ETP)

For messages larger than 1,785 bytes (255 packets × 7 bytes), J1939 defines the Extended Transport Protocol (ETP). ETP uses the same flow control model as CMDT but with extended packet counts and 32-bit message sizes.

ETP is defined in J1939-21 Annex A. ETP uses two PGNs: ETP.CM (PGN 0x00C800, 51200) for connection management and ETP.DT (PGN 0x00C700, 50944) for data transfer. The ETP.CM RTS message contains: byte 1 = control byte (0x14 for RTS), bytes 2-5 = total message size (up to 117,440,505 bytes), bytes 6-8 = PGN being transferred.

4.1 ETP Frame Format

ETP.CM RTS (PGN 51200 / 0x00C800):
  Byte 1:    0x14 (RTS control byte)
  Bytes 2-5: Total message size in bytes (little-endian, 32-bit)
  Bytes 6-8: PGN being transported (little-endian, 3 bytes)

ETP.DT (PGN 50944 / 0x00C700):
  Byte 1:    Sequence number
  Bytes 2-8: 7 data bytes per packet

4.2 ETP vs. Standard TP Comparison

Feature TP (BAM/CMDT) ETP
Max message size 1,785 bytes 117,440,505 bytes
Max packets 255 16,777,215
PGN for CM 60416 (0xEC00) 51200 (0x00C800)
PGN for DT 60160 (0xEB00) 50944 (0x00C700)
Use case Most multi-packet messages Large data transfers (firmware, configs)
Defined in J1939-21 §5.10 J1939-21 Annex A

📝 Note: ETP.CM uses PGN 51200 (0x00C800) and ETP.DT uses PGN 50944 (0x00C700). These are distinct from the standard TP PGNs and must be handled by a separate protocol layer in your implementation.

Extended Transport Protocol (ETP) — Worked Example

ETP handles payloads from 1,786 bytes up to 117,440,505 bytes. It extends BAM/CMDT with 32-bit byte counts and larger sequence numbers. The flow control model mirrors CMDT (RTS/CTS handshake), but the RTS message uses a 4-byte size field instead of the 2-byte field in standard TP, and sequence numbers can exceed 255 to accommodate the much larger payloads.

import can

def build_etp_rts(pgn: int, total_size: int, source: int, dest: int) -> can.Message:
    """Build ETP.CM RTS (Request To Send) message.

    Args:
        pgn: PGN being transported
        total_size: Total payload size in bytes
        source: Source address
        dest: Destination address
    """
    data = bytes([
        0x14,                           # ETP RTS control byte
        total_size & 0xFF,              # Total size byte 1 (LSB)
        (total_size >> 8) & 0xFF,       # Total size byte 2
        (total_size >> 16) & 0xFF,      # Total size byte 3
        (total_size >> 24) & 0xFF,      # Total size byte 4 (MSB)
        pgn & 0xFF,                     # PGN byte 1
        (pgn >> 8) & 0xFF,             # PGN byte 2
        (pgn >> 16) & 0xFF,            # PGN byte 3
    ])

    arb_id = (0x1CC80000 | (dest << 8) | source)
    return can.Message(arbitration_id=arb_id, data=data, is_extended_id=True)

# Example: Sending a 2000-byte payload via ETP
total_size = 2000
num_packets = (total_size + 6) // 7
print(f"ETP transfer: {total_size} bytes")
print(f"  Data packets needed: {num_packets}")
print(f"  Transfer time at 100ms spacing: {num_packets * 0.1:.1f} seconds")

# Build the ETP RTS frame for this transfer
rts_msg = build_etp_rts(pgn=0xFECA, total_size=total_size, source=0x00, dest=0x21)
print(f"  ETP.CM RTS arbitration ID: 0x{rts_msg.arbitration_id:08X}")
print(f"  ETP.CM RTS data: {rts_msg.data.hex(' ')}")

Expected output:

ETP transfer: 2000 bytes
  Data packets needed: 286
  Transfer time at 100ms spacing: 28.6 seconds
  ETP.CM RTS arbitration ID: 0x1CC82100
  ETP.CM RTS data: 14 d0 07 00 00 ca fe 00

The arbitration ID 0x1CC82100 encodes PGN 0x00C800 (ETP.CM = 51200), destination address (DA) 0x21, and source address (SA) 0x00. The data field shows the RTS control byte (0x14), the 32-bit total size (2000 = 0x000007D0, little-endian), and the PGN being transported (0x00FECA = DM1).


5. DM Messages (Diagnostic Messages)

J1939-73 defines a comprehensive set of diagnostic messages. DM1 is by far the most commonly used, but the others provide important diagnostic capabilities.

5.1 DM1 — Active Diagnostic Trouble Codes

PGN 65226 (0xFECA). Broadcast periodically (default: 1000 ms) by every Electronic Control Unit (ECU) that has active DTCs — or with “no active DTCs” status if the ECU is healthy.

packet-beta
  0-7: "Lamp Status Byte 1"
  8-15: "Lamp Status Byte 2"
  16-23: "DTC 1 — SPN [7:0]"
  24-31: "DTC 1 — SPN [15:8]"
  32-36: "SPN [18:16] (3b) | FMI (5b)"
  37-39: "CM|OC (1b+7b)"
  40-47: "DTC 2 — SPN [7:0]"
  48-55: "DTC 2 — SPN [15:8]"
  56-60: "SPN [18:16] | FMI"
  61-63: "CM|OC"

Figure: J1902 03 dm1 structure

“No Active DTCs” DM1 Format:

When no active DTCs are present, a node transmits a DM1 message with lamp status bytes set to 0x00 and a single DTC field of 0x00 0x00 0x00 0x00 (SPN=0, FMI=0, OC=0). The total data length is 8 bytes, transmitted as a single CAN frame (no BAM required).

No-DTC DM1 (8 bytes, single CAN frame):
  Byte 1: 0x00  (all lamps off)
  Byte 2: 0x00  (no lamp flash)
  Bytes 3-6: 0x00 0x00 0x00 0x00  (SPN=0, FMI=0, OC=0)
  Bytes 7-8: 0xFF 0xFF  (padding)

DM1 Message Structure:

Bytes 1-2: Lamp Status
  Byte 1, bits 7-6: Malfunction Indicator Lamp (MIL)
  Byte 1, bits 5-4: Red Stop Lamp (RSL)
  Byte 1, bits 3-2: Amber Warning Lamp (AWL)
  Byte 1, bits 1-0: Protect Lamp (PL)
  Byte 2, bits 7-6: MIL flash
  Byte 2, bits 5-4: RSL flash
  Byte 2, bits 3-2: AWL flash
  Byte 2, bits 1-0: PL flash

  Lamp status encoding:
    00 = Slow flash
    01 = Fast flash
    10 = On (continuous)
    11 = Off / not available

Bytes 3+: DTC entries (4 bytes each)

5.2 J1939 DTC Structure (4 bytes per DTC)

Byte layout:

Byte 1: SPN bits 7–0 (least significant 8 bits)
Byte 2: SPN bits 15–8 (middle 8 bits)
Byte 3: SPN bits 18–16 (upper 3 bits, bits 7-5) | FMI bits 4-0 (bits 4-0)
Byte 4: CM (bit 7) | OC bits 6-0 (bits 6-0)
def decode_j1939_dtc(byte1, byte2, byte3, byte4):
    """Decode a 4-byte J1939 DTC."""
    spn = byte1 | (byte2 << 8) | ((byte3 >> 5) << 16)
    fmi = byte3 & 0x1F
    cm = (byte4 >> 7) & 0x01
    oc = byte4 & 0x7F

    return {
        'SPN': spn,
        'FMI': fmi,
        'OC': oc,
        'CM': cm
    }

# Example DTC bytes
dtc = decode_j1939_dtc(0x9C, 0x05, 0x11, 0x03)
print(f"SPN: {dtc['SPN']}, FMI: {dtc['FMI']}, OC: {dtc['OC']}, CM: {dtc['CM']}")
# Output
SPN: 1436, FMI: 17, OC: 3, CM: 0

Additional DTC Decoding Examples:

# Example 2: SPN 100, FMI 1, OC 5, CM 0 — Engine Oil Pressure low
#   Byte 1: 0x64 (SPN bits 7-0 = 100)
#   Byte 2: 0x00 (SPN bits 15-8 = 0)
#   Byte 3: 0x01 (SPN bits 18-16 = 0, FMI = 1)
#   Byte 4: 0x05 (CM = 0, OC = 5)
dtc2 = decode_j1939_dtc(0x64, 0x00, 0x01, 0x05)
print(f"SPN: {dtc2['SPN']}, FMI: {dtc2['FMI']}, OC: {dtc2['OC']}, CM: {dtc2['CM']}")
# Output: SPN: 100, FMI: 1, OC: 5, CM: 0
# Real-world: Engine Oil Pressure (SPN 100) is below normal range (FMI 1)
#   with 5 occurrences — likely oil pump degradation or low oil level.

# Example 3: SPN 3226, FMI 2, OC 12, CM 1 — Aftertreatment SCR Catalyst
#   SPN 3226 = 0xC9A → byte1 = 0x9A, byte2 = 0x0C, upper 3 bits = 0
#   FMI 2 → erratic/intermittent data
#   OC 12, CM 1 (conversion method 1 — see J1939-73 for CM interpretation)
#   Byte 3: (0 << 5) | 2 = 0x02
#   Byte 4: (1 << 7) | 12 = 0x8C
dtc3 = decode_j1939_dtc(0x9A, 0x0C, 0x02, 0x8C)
print(f"SPN: {dtc3['SPN']}, FMI: {dtc3['FMI']}, OC: {dtc3['OC']}, CM: {dtc3['CM']}")
# Output: SPN: 3226, FMI: 2, OC: 12, CM: 1
# Real-world: Aftertreatment SCR Catalyst Intake NOx (SPN 3226) showing
#   erratic readings (FMI 2) with 12 occurrences and CM=1 — this is a
#   chronic emissions-critical fault likely requiring sensor replacement.

Interpreting DTC output in context: For example, SPN 100 (Engine Oil Pressure) with FMI 1 (Data Valid But Below Normal Operating Range) and OC 5 means the engine oil pressure sensor has reported low readings on 5 separate occasions — this likely indicates an oil pump issue, a clogged oil filter, or low oil level. The Occurrence Count (OC) is particularly valuable for distinguishing intermittent issues (OC 1–2) from chronic problems (OC > 5) that require immediate attention.

5.3 Failure Mode Identifiers (FMI)

FMI Description
0 Data valid but above normal operational range — most severe
1 Data valid but below normal operational range — most severe
2 Data erratic, intermittent, or incorrect
3 Voltage above normal, or shorted to high source
4 Voltage below normal, or shorted to low source
5 Current below normal or open circuit
6 Current above normal or grounded circuit
7 Mechanical system not responding or out of adjustment
8 Abnormal frequency or pulse width or period
9 Abnormal update rate
10 Abnormal rate of change
11 Root cause not known
12 Bad intelligent device or component
13 Out of calibration
14 Special instructions
15 Data valid but above normal operating range — least severe
16 Data valid but above normal operating range — moderately severe
17 Data valid but below normal operating range — least severe
18 Data valid but below normal operating range — moderately severe
19 Received network data in error
20–30 Reserved
31 Condition exists

5.4 DM2 through DM13

DM PGN Name Purpose
DM1 65226 Active DTCs Currently active fault codes and lamp status
DM2 65227 Previously Active DTCs Faults that were active but are no longer present
DM3 65228 Diagnostic Data Clear Clear all DTCs and diagnostic data (request)
DM4 65229 Freeze Frame Parameters Snapshot of parameters when DTC was set
DM5 65230 Diagnostic Readiness 1 Monitor readiness status (similar to OBD2 readiness)
DM6 65231 Emission-Related Pending DTCs DTCs detected but not yet confirmed
DM7 65232 Command Non-Continuously Monitored Test Request specific diagnostic tests
DM8 65233 Test Results for Non-Continuously Monitored Systems Results of DM7-requested tests
DM11 65235 Diagnostic Data Clear for Active DTCs Clear active DTCs only
DM12 65236 Emission-Related Active DTCs Active DTCs that are emissions-related
DM13 65237 Stop/Start Broadcast Command ECUs to stop or start DM1/DM2 broadcasts

📝 Note: DM9 and DM10 are not consistently defined across all versions of J1939-73. DM9 (Request for DM8 Test Results) uses PGN 0xFECC. DM10 (Request for DM8 Test Results — not defined in all versions) — consult J1939-73 for the specific version applicable to your target year. DM messages beyond DM13 (DM14–DM53 and higher) provide additional capabilities including memory access, boot load, and extended freeze frames, but they are rarely implemented in basic J1939 applications.

DM2–DM13 Detailed Coverage

DM2 — Previously Active DTCs (PGN 65227, 0xFECB):

DM2 reports DTCs that were active but have since cleared (either self-healed or cleared by a DM3/DM11 request). The format is identical to DM1 — same lamp bytes, same 4-byte DTC structure. DM2 is requested via PGN Request (0xEA00) from the diagnostic tool. Unlike DM1 (which is broadcast periodically), DM2 is only sent in response to a request. This distinction is important when designing diagnostic applications: you must actively poll for DM2 data, whereas DM1 data arrives passively. The previously-active DTC list is valuable for identifying intermittent faults that have temporarily resolved — a DTC appearing in DM2 but not DM1 indicates the fault condition is no longer present but was detected at some point since the last clear operation.

DM3 — Diagnostic Data Clear/Reset (PGN 65228):

DM3 clears all previously active DTCs (DM2 data). The request is sent to a specific Source Address (SA) or broadcast. Safety implication: Clearing DTCs before a fault is fully resolved may mask ongoing problems. In regulated vehicles (EPA/CARB), clearing DTCs resets readiness monitors, potentially causing emission inspection failure. Format: The request contains bytes indicating which memory to clear (0xFF 0xFF 0xFF 0xFF for all). DM3 should be used with care in fleet management scenarios — clearing historical fault data removes evidence needed for root cause analysis. Best practice is to log all DM2 data to an off-vehicle system before issuing a DM3 clear.

DM5 — Diagnostic Readiness (PGN 65230):

DM5 reports OBD compliance status and readiness monitor completion, analogous to OBD-II Service 0x01 PID 0x01. Byte 1 = number of active DTCs, Byte 2 = number of previously active DTCs, Byte 3 = OBD compliance level. The readiness monitors indicate whether the vehicle’s self-diagnostic systems have completed their checks since the last DTC clear. An incomplete monitor means the system has not yet had the opportunity to test a particular subsystem — this is common after a DTC clear or battery disconnect. In emissions inspection contexts, incomplete monitors may result in a test rejection rather than a pass or fail.

DM11 — Diagnostic Data Clear/Reset for Active DTCs (PGN 65235):

DM11 clears active DTCs (DM1 data) and resets associated freeze frame and lamp information. Unlike DM3, DM11 targets active (not previously active) codes. Use with caution in production environments — clearing active DTCs on a running engine may trigger immediate re-detection if the underlying fault condition persists. DM11 is typically used during service procedures after a repair has been completed, to verify that the fault does not return. If a DTC reappears within one drive cycle after a DM11 clear, the repair was likely ineffective.

DM12 — Emissions-Related Active DTCs (PGN 65236):

DM12 contains only the emissions-related subset of DM1. Format matches DM1. Used by regulatory inspection equipment to isolate emission-relevant faults from general vehicle DTCs. The filtering of which DTCs are emissions-related is performed by the transmitting ECU based on its internal configuration — the same SPN/FMI combination may appear in DM1 but not DM12 if the ECU does not classify it as emissions-relevant. When developing aftertreatment or engine control diagnostics, ensure that all emissions-critical DTCs are properly flagged for inclusion in DM12 responses.

Complete DM1 Reassembly — End-to-End

The following class demonstrates end-to-end DM1 reception, handling both single-frame DM1 messages (1 DTC or fewer) and multi-frame DM1 messages arriving via BAM transport. This is a common requirement in telematics gateways and diagnostic tools.

import can
import struct
from collections import defaultdict

class DM1Receiver:
    """Receive and decode DM1 messages (active DTCs) from J1939 bus.

    DM1 messages ≤8 bytes arrive as single frames.
    DM1 messages >8 bytes arrive via BAM transport.
    """

    def __init__(self):
        self.bam_sessions = {}  # source_address -> session dict

    def process_frame(self, msg: can.Message) -> dict | None:
        """Process a CAN frame. Returns decoded DM1 if complete, else None."""
        arb_id = msg.arbitration_id
        pgn = (arb_id >> 8) & 0x3FFFF
        sa = arb_id & 0xFF

        # Single-frame DM1 (PGN 0xFECA = 65226)
        if pgn == 0xFECA:
            return self._decode_dm1(msg.data, sa)

        # BAM announcement (TP.CM, PGN 0xEC00 + dest)
        if (pgn & 0xFF00) == 0xEC00:
            if msg.data[0] == 0x20:  # BAM
                return self._handle_bam_announce(msg.data, sa)

        # BAM data packet (TP.DT, PGN 0xEB00 + dest)
        if (pgn & 0xFF00) == 0xEB00:
            return self._handle_bam_data(msg.data, sa)

        return None

    def _handle_bam_announce(self, data, sa):
        """Process BAM announcement."""
        total_size = data[1] | (data[2] << 8)
        num_packets = data[3]
        pgn = data[5] | (data[6] << 8) | (data[7] << 16)

        if pgn == 0xFECA:  # DM1
            self.bam_sessions[sa] = {
                'total_size': total_size,
                'num_packets': num_packets,
                'received': {},
            }
        return None

    def _handle_bam_data(self, data, sa):
        """Process BAM data packet. Returns DM1 if reassembly complete."""
        if sa not in self.bam_sessions:
            return None  # Orphaned packet — discard

        session = self.bam_sessions[sa]
        seq = data[0]
        session['received'][seq] = data[1:8]

        # Check if all packets received
        if len(session['received']) >= session['num_packets']:
            # Reassemble
            payload = b''
            for i in range(1, session['num_packets'] + 1):
                payload += session['received'].get(i, b'\xFF' * 7)
            payload = payload[:session['total_size']]

            del self.bam_sessions[sa]
            return self._decode_dm1(payload, sa)

        return None

    def _decode_dm1(self, data, sa):
        """Decode DM1 payload into structured data."""
        result = {
            'source_address': sa,
            'lamp_status': {
                'protect': (data[0] >> 0) & 0x03,
                'amber_warning': (data[0] >> 2) & 0x03,
                'red_stop': (data[0] >> 4) & 0x03,
                'malfunction': (data[0] >> 6) & 0x03,
            },
            'dtcs': []
        }

        # Parse DTCs (4 bytes each, starting at byte 2)
        offset = 2
        while offset + 3 <= len(data):
            spn_low = data[offset]
            spn_mid = data[offset + 1]
            spn_fmi_byte = data[offset + 2]
            oc_byte = data[offset + 3]

            # SPN: 19 bits (bytes 0-1 full, byte 2 bits 7-5)
            spn = spn_low | (spn_mid << 8) | ((spn_fmi_byte >> 5) << 16)
            fmi = spn_fmi_byte & 0x1F
            cm = (oc_byte >> 7) & 0x01
            oc = oc_byte & 0x7F

            if spn == 0 and fmi == 0 and oc == 0:
                break  # No more DTCs (padding or "no active DTCs")

            result['dtcs'].append({
                'spn': spn,
                'fmi': fmi,
                'cm': cm,
                'oc': oc,
            })
            offset += 4

        return result

# === Usage Example ===
receiver = DM1Receiver()
bus = can.Bus(channel='can0', interface='socketcan')

print("Listening for DM1 messages (Ctrl+C to stop)...")
try:
    while True:
        msg = bus.recv(timeout=1.0)
        if msg is None:
            continue

        dm1 = receiver.process_frame(msg)
        if dm1:
            sa = dm1['source_address']
            print(f"\nDM1 from SA 0x{sa:02X}:")
            lamps = dm1['lamp_status']
            active_lamps = [k for k, v in lamps.items() if v == 1]
            print(f"  Lamps: {', '.join(active_lamps) if active_lamps else 'none'}")

            if dm1['dtcs']:
                for dtc in dm1['dtcs']:
                    print(f"  DTC: SPN {dtc['spn']} / FMI {dtc['fmi']} "
                          f"/ OC {dtc['oc']}")
            else:
                print("  No active DTCs")
except KeyboardInterrupt:
    pass
finally:
    bus.shutdown()

Expected output (when receiving the BAM example from Section 2.3):

DM1 from SA 0x00:
  Lamps: none
  DTC: SPN 100 / FMI 1 / OC 3
  DTC: SPN 110 / FMI 0 / OC 1
  DTC: SPN 190 / FMI 2 / OC 7

6. Timeout and Error Handling

6.1 Transport Protocol Error Scenarios

Error Cause Detection Response
RTS with no CTS Receiver busy or offline Sender timeout T1 (750 ms) Sender retries or aborts
Missing DT packet Packet lost on bus Receiver timeout T3 (1250 ms) Receiver sends Connection Abort
Wrong sequence number Corrupted packet or implementation error Receiver detects SN gap Receiver sends Connection Abort
CTS with 0 packets Receiver requests hold (flow control) Sender sees CTS with count = 0 Sender waits for next CTS (up to timeout)
Duplicate BAM Sender retransmits (allowed) Receiver detects duplicate SN Receiver overwrites previous data for that SN

6.2 Orphaned and Malformed Packets

Orphaned TP.DT packets: Orphaned TP.DT packets (data transfer packets received without a preceding TP.CM/BAM announcement) should be silently discarded. They typically result from the receiver missing the announcement due to bus congestion or a late join to the network. Do not attempt to reassemble data from orphaned packets, as there is no way to determine the target PGN, total message size, or expected packet count.

Malformed BAM transfers: If a BAM.DT packet is lost or arrives out of sequence, the reassembled data will be corrupted. Implement a sequence-number check: if the received sequence number does not equal the expected next sequence number, discard the entire transfer and reset the reassembly state. Wait for the next BAM announcement to begin a fresh transfer.

6.3 Implementation Best Practices

  1. Always implement timeouts — never wait indefinitely for a TP.DT or TP.CM frame
  2. Handle abort gracefully — log the abort reason and clean up session state
  3. Support concurrent sessions — a node may participate in multiple CMDT sessions simultaneously (one per PGN per destination)
  4. Respect CTS pacing — do not send more DT packets than the CTS allows
  5. Pad the last packet — the last TP.DT packet must be padded with 0xFF to fill all 7 data bytes

Troubleshooting

# Symptom Likely cause Diagnostic step Resolution
1 BAM transfer missing packets Bus congestion or timing violation (packets too fast/slow) Check inter-packet timing with a CAN analyzer — must be 50–200 ms per J1939-21 §5.10.2.2. Log timestamps of all TP.DT packets and verify gaps fall within range Adjust sender timing to target 100 ms inter-packet delay; reduce overall bus load below 50%; verify no other high-priority traffic is preempting BAM frames
2 CMDT aborts immediately after RTS Receiver does not support CMDT for the requested PGN, or receiver is already in a session for this PGN Check abort reason code in the Connection Abort frame (byte 2): reason 1 = already in session, reason 2 = insufficient resources If reason 1: wait for the existing session to complete before retrying. If reason 2: reduce message size or use BAM. Verify PGN filter list includes 0xFECA (DM1) and 0xFECB (DM2) on the receiver
3 DM1 shows SPN 0, FMI 0 No active DTCs — this is the “all clear” DTC entry Check lamp status bytes — all lamps should show “off” (0x00 0x00 for lamp status). Verify message is 8 bytes with DTC field 0x00 0x00 0x00 0x00 Normal operation — no action needed. If lamp bytes are non-zero with SPN 0, suspect a firmware bug in the transmitting ECU
4 SPN value decodes to wrong parameter SPN bit extraction error (19-bit field spans 3 bytes) Verify byte-level extraction: SPN = byte1 (byte2<<8)
5 DM1 message is 8 bytes but has more than 1 DTC Only 1 DTC fits in a single 8-byte frame (2 lamp + 4 DTC + 2 padding) Check if BAM is being used for multi-DTC DM1. Filter for TP.CM with PGN 0xFECA in bytes 6-8 Monitor for TP.CM_BAM with PGN 0xFECA; multi-DTC DM1 uses BAM transport. Ensure your TP reassembly code is active and correctly mapping PGN 0xFECA to DM1 parsing
6 CMDT transfer hangs after CTS CTS requested 0 packets (hold), but sender never gets a follow-up CTS Check for CTS frames with packet count = 0 (hold messages); count consecutive holds and log timestamps Add timeout handling for CTS-hold; abort after 3+ consecutive holds or if total hold duration exceeds T1 (750 ms). Note: CTS hold is non-normative — not all stacks support it
7 ETP transfer not starting Device does not support ETP; only standard TP Check if the data exceeds 1,785 bytes; verify ETP support on both devices by checking documentation or sending a test ETP.CM RTS If ETP not supported, break the transfer into chunks ≤ 1,785 bytes. Verify both devices handle PGN 51200 (ETP.CM) and PGN 50944 (ETP.DT)
8 Reassembled BAM data contains garbage A BAM.DT packet was lost or arrived out of sequence Log all TP.DT packets with sequence numbers — look for gaps or duplicates. Compare expected packet count from BAM announcement against received TP.DT count Implement a sequence-number check in your reassembly code. If a gap is detected, discard the entire transfer and wait for the next BAM announcement. Do not attempt partial reassembly
9 Unexpected TP.DT packets with no matching session Orphaned TP.DT packets received without a preceding TP.CM/BAM announcement Check if the listener joined the bus after a BAM announcement was already sent. Log TP.CM frames to confirm announcements are being received Silently discard orphaned TP.DT packets. They typically result from the receiver missing the announcement due to bus congestion or a late join to the network

References

  1. SAE J1939-21:2018 — Data Link Layer. Defines BAM, CMDT, and ETP transport protocols.

  2. SAE J1939-73:2019 — Application Layer — Diagnostics. Defines DM1–DM13+ diagnostic messages.

  3. SAE J1939-71:2020 — Vehicle Application Layer. SPN and PGN definitions.

  4. SAE J1939-81:2017 — Network Management. Address claiming and NAME.

  5. Kvaser J1939 Transport Protocol Guide — Practical guide to BAM and CMDT implementation.

  6. Open-SAE-J1939 — Open-source J1939 library with TP implementation. Repository: github.com/DanielMartensson/Open-SAE-J1939.


Changelog

Version Date Author Summary of changes
1.0 2026-03-16 Telematics Tutorial Series Initial publication

```