J1939 Diagnostic Messages - dfieschko/RP1210 GitHub Wiki
This package includes some tools for parsing and generating J1939 diagnostic messages (e.g. DM1, DM2, DM12).
J1939.DiagnosticMessage Class
DiagnosticMessage stores lamp codes (2 bytes) and Diagnostic Trouble Codes (n*4 bytes), and can be used to either parse diagnostic messages you've received or to generate diagnostic messages to transmit.
Properties
Diagnostic Message properties are accessed directly. Modifying one property will automatically modify the others.
data
- J1939 message data (type: 
bytes) - Doesn't include PGN, etc - only message data
 - First 2 bytes hold lamp codes
 - Subsequent n*4 bytes hold DTCs
 
lamps
- Contains lamp codes (2 bytes) (type: 
bytes) - Byte 0 = MIL, RSL, AWL, and PL lamp codes (3 bits each)
 - Bytes 1 = proprietary
 
codes
- Contains list of DTC objects (type: 
list[DTC]) - Iterating over DiagnosticMessage will iterate over 
codes 
Methods
to_dtcs(data : bytes) -> list[DTC]
Parses given J1939 message data (type:bytes) into a list of DTCs (diagnostic trouble codes) (type: list[DTC]). This is a static method, so you don't need to initialize a DiagnosticMessage class to use it.
- param 
data(bytes): J1939 message data - returns list of DTCs parsed from message data (
list[DTC]) 
DiagnosticMessage uses this method to generate its codes property. Therefore, the following two lines will result in equivalent values for dtc_list:
dtc_list = DiagnosticMessage.to_dtcs(j1939_msg_data)
dtc_list = DiagnosticMessage(j1939_msg_data).codes
num_dtcs() -> int
num_dtcs() returns the number of DTCs contained in the DiagnosticMessage. This is equivalent to len(DiagnosticMessage.codes).
Lamp Codes - mil(), rsl(), awl(), pwl() -> int
The lamp code functions return 3-bit lamp codes corresponding with the lamp they represent (as ints). These are stored in byte 0 of the class's lamps property. There are four Lamp Code functions, each of which has four possible states: Lamp OFF, Lamp ON, Lamp Flashing 1 Hz, and Lamp Flashing 2 Hz.
mil(): Malfunction Indicator Lamprsl(): Red Stop Lampawl(): Amber Warning Lamppl(): Protection Lamp
Magic Methods
__getitem__ and __setitem__
These allow a DiagnosticMessage object to be treated like a list of DTCs, which makes it convenient to process the meat of a diagnostic message. Accessing DiagnosticMessage as a list has the same effect as accessing the DiagnosticMessage.code property.
dm1_msg = DiagnosticMessage(j1939_data)
# the following two lines are equivalent to each other:
first_dtc = dm1_msg[0]
first_dtc = dm1_msg.codes[0]
# iterating through DTCs is as simple as:
for dtc in dm1_msg:
   process_dtc(dtc)
__iadd__
This allows you to use the += operator to directly add DTCs to a DiagnosticMessage's codes list.
dm1_msg += DTC(dtc_data)
__bytes__
Allows a DiagnosticMessage object to be converted to bytes (returns data property). This allows a DiagnosticMessage object to be used as the data parameter for J1939Message objects.
msg_data = bytes(dm1_msg) # is equivalent to:
msg_data = dm1_msg.data
j1939_msg = J1939Message(pgn=0xFECA, pri=6, sa=0xF9, data=dm1_msg)
__int__
int(DiagnosticMessage) will return an integer representation of the data property.
__str__
str(DiagnosticMessage) will return a str representation of the data property (mostly useful for print statements, etc).
__len__
len(DiagnosticMessage) will return the number of DTCs contained in the DiagnosticMessage object. It's like this to preserve consistency with __getitem__ and __setitem__.
__bool__
DiagnosticMessage objects will evaluate to True if they contain DTCs, or False if they don't.
__eq__
Directly compares data property with another DiagnosticMessage or bytes.
DiagnosticMessage Examples
Initializing DiagnosticMessage
msg_data = rp1210_client.rx()
# initialize from J1939Message
dm1_msg = DiagnosticMessage(J1939Message(msg_data))
# initialize directly from message data
dm1_msg = DiagnosticMessage(msg_data)
# create your own
dm1_msg = DiagnosticMessage()
... # add DTCs, modify lamp codes, etc
Reading Diagnostic Trouble Codes
# access dtcs through codes property:
dtc_list = dm1_msg.codes # where dm1_msg is instance of DiagnosticMessage
# iterate through DTCs directly:
for dtc in dm1_msg:
   process_dtc(dtc)
Reading Lamp Codes
# where dm1_msg is instance of DiagnosticMessage:
malfunction_indicator_code = dm1_msg.mil()
red_stop_lamp_code = dm1_msg.rsl()
amber_warning_lamp_code = dm1_msg.awl()
protection_lamp_code = dm1_msg.pl()
Generating Diagnostic Messages
dtc1 = DTC(spn=423, fmi=12, oc=80)
dtc2 = DTC(spn=100, fmi=1, oc=2)
dm1_msg = DiagnosticMessage() # init
dm1_msg += dtc1 # add DTCs
dm1_msg += dtc2
j1939_msg = J1939Message(pgn=0xFECA, pri=6, sa=0xF9, data=dm1_msg)
rp1210_client.tx(j1939_msg)
Minimal Working Example
# Setup for minimal working example
from RP1210 import RP1210Client
from RP1210.J1939 import J1939Message, DiagnosticMessage, DTC
VENDOR = "NULN2R32" # Nexiq USB-Link 2
DEVICE = 1          # this vendor/device combo will only work with USB-Link 2
# Set up client
client = RP1210Client()
client.setVendor(VENDOR)
client.setDevice(DEVICE)
client.connect()
while True:
   # Receive data
   data_bytes = client.rx()
   j1939_msg = J1939Message(data_bytes)
   # Process DM1s
   if j1939_msg.pgn == 0xFECA:
      # init DiagnosticMessage
      dm1_msg = DiagnosticMessage(j1939_msg)  # init with J1939Message
      dm1_msg = DiagnosticMessage(data_bytes) # OR init with data directly from ReadMessage
   
      # access properties
      dtc_list   = dm1_msg.codes # type: list[DTC] (n*4 bytes)
      lamp_codes = dm1_msg.lamps # type: bytes (2 bytes)
      dm1_data   = dm1_msg.data  # type: bytes (2 bytes + n*4 bytes)
      # iterating over DiagnosticMessage will iterate over its DTCs (see J1939.DTC)
      for dtc in dm1_msg:
         print(f"SPN: {dtc.spn} FMI: {dtc.fmi} OC: {dtc.oc}")
      # functions for parsing lamp codes
      if dm1_msg.mil(): # 3-bit code
         print("Malfunction Indicator Lamp on!")
      if dm1_msg.rsl(): # 3-bit code
         print("Red Stop Lamp on!")
      if dm1_msg.awl(): # 3-bit code
         print("Amber Warning Lamp on!")
      if dm1_msg.pl(): # 3-bit code
         print("Protection Lamp on!")
J1939.DTC Class
Diagnostic Trouble Codes (DTCs) reported in a DiagnosticMessage are each composed of four bytes and contain integer values for a fault's SPN (Suspect Parameter Number), FMI (Failure Mode Identifier), and Occurrence Count (OC). The class J9139.DTC can be used to parse or generate these codes.
Properties
DTC properties are accessed directly. Modifying one property will automatically modify the others.
data
- DTC data, composed of 4 bytes (type: 
bytes) - Contains data for 
spn,fmi, andoc(andcm()). 
spn
- Suspect Parameter Number (type: 
int) - 19 bits, making up bytes 0, 1, and part of byte 2.
 
fmi
- Failure Mode Identifier (type: 
int) - 5 bits, making up part of byte 2.
 
oc
- Occurrence Count (type: 
int) - 8 bits, making up byte 3.
 
Methods
cm() -> int
- Returns Conversion Method bit as int.
 - Will be 0 in most cases.
 
Magic Methods
__getitem__ and __setitem__
These allow a DTC object's data property to be accessed and modified directly by treating DTC as a list of bytes.
__iadd__
Allows oc (Occurrence Count) to be incremented by incrementing DTC object directly.
dtc = DTC(spn=10, fmi=10, oc=20)
dtc += 1 # OC will now be 21
__bytes__
Allows a DTC object to be converted to bytes (returns data property).
__int__
int(DTC) will return an integer representation of the data property.
__str__
str(DTC) will return a str representation of the data property (mostly useful for print statements, etc).
__len__
Returns len(DTC.data).
__eq__
Directly compares data property with another DTC or bytes.
__bool__
DTC will evaluate to True if it contains information, or False if it doesn't.