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
- Always implement timeouts — never wait indefinitely for a TP.DT or TP.CM frame
- Handle abort gracefully — log the abort reason and clean up session state
- Support concurrent sessions — a node may participate in multiple CMDT sessions simultaneously (one per PGN per destination)
- Respect CTS pacing — do not send more DT packets than the CTS allows
- 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
-
SAE J1939-21:2018 — Data Link Layer. Defines BAM, CMDT, and ETP transport protocols.
-
SAE J1939-73:2019 — Application Layer — Diagnostics. Defines DM1–DM13+ diagnostic messages.
-
SAE J1939-71:2020 — Vehicle Application Layer. SPN and PGN definitions.
-
SAE J1939-81:2017 — Network Management. Address claiming and NAME.
-
Kvaser J1939 Transport Protocol Guide — Practical guide to BAM and CMDT implementation.
-
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 |
```