From 2a1412330b58a6eea010d724b690034dea7175c1 Mon Sep 17 00:00:00 2001 From: Austin Murphy Date: Mon, 23 Jul 2012 11:34:55 -0400 Subject: [PATCH] Updated decode routines, improve feature pid decoding mode 9 message length checking and basic IPT info --- obd2.py | 192 +++++++++++++++++++++++++++++++++++++--------- obd2_reader.py | 152 ++++++++++++++++++++++++++++-------- obd2_std_PIDs.csv | 10 +-- scan-to-trace.py | 6 +- 4 files changed, 282 insertions(+), 78 deletions(-) diff --git a/obd2.py b/obd2.py index d0c8174..1ba7d08 100644 --- a/obd2.py +++ b/obd2.py @@ -325,7 +325,9 @@ def decode_text( data ) : TXT = '' for d in data: # check that result is actually ascii - if d >= '20' and d <= '7E': + #if d >= '20' and d <= '7E': + hd = eval( "0x" + d ) + if hd >= 0x20 and hd <= 0x7E : TXT += binascii.unhexlify(d) else: TXT += '_' @@ -465,16 +467,16 @@ def decode_data_by_mode(mode, pid, data): PID = M + P #print "PiID: -", PID, "-" - # feature PIDs are the same for all modes - if PID in feature_PIDs: - return decode_feature_pid(PID, D) + ## feature PIDs are the same for all modes + ## no, mode9 has a message count + #if PID in feature_PIDs: + # return decode_feature_pid(PID, D) if M == '01' : return decode_mode1_pid(PID, D) - #PID = '01' + str.upper(P).rjust(2,'0') elif M == '02' : - # mode 2 uses the same definitions as mode 1 (I hope...) + # mode 2 uses the same definitions as mode 1 if P == '02' : return decode_DTCs(data) else : @@ -485,24 +487,14 @@ def decode_data_by_mode(mode, pid, data): return decode_DTCs(data) elif M == '04' : - # when mode 04 is sent, the DTCs are cleared. There is no response. + # when mode 04 is sent, the DTCs are cleared. There is no response, except "44" or and error. pass elif M == '05' : - #PID = '05' + str.upper(P).rjust(4,'0') - pass + return decode_mode5_pid(PID, data) elif M == '06' : - PID = '06' + str.upper(P).rjust(2,'0') - print " DONT KNOW how to interpret mode 06" - print " PID:", PID - # same as hex - #print " Raw:", data - print " Hex:", decode_hex(data) - print " Ints:", decode_ints(data) - # text needs to be filtered - #print " Text:", decode_text(data) - return [] + return decode_mode6_pid(PID, data) elif M == '07' : return decode_DTCs(data) @@ -511,7 +503,6 @@ def decode_data_by_mode(mode, pid, data): pass elif M == '09' : - #PID = '09' + str.upper(P).rjust(2,'0') return decode_mode9_pid(PID, D) elif M == '0A' : @@ -578,6 +569,9 @@ def decode_mode1_pid(PID, data): values.append( ["Unknown PID: " + PID, decode_hex(data), ""] ) return [] + elif PID in feature_PIDs: + return decode_feature_pid(PID, data) + elif PID == '0101' or PID == '0141': return decode_monitors(PID, data) @@ -619,7 +613,11 @@ def decode_mode1_pid(PID, data): elif PID == '011C': A = data[0] - values.append( ["OBD standard", A, OBD_standards[A]] ) + print "DEBUG: byte A:", A + if A in OBD_standards : + values.append( ["OBD standard", A, OBD_standards[A]] ) + else : + values.append( ["OBD standard", A, "Unknown"] ) return values elif PID == '0151': @@ -633,42 +631,162 @@ def decode_mode1_pid(PID, data): return decode_generic_pid(PID, data) +def decode_mode5_pid(PID, data): + """ Decode Mode5 . """ + # NOTE: this is only for OLD style (not CAN) + + values = [] + + if PID not in PIDs : + #print "Unknown PID, data: ", PID, data + values.append( ["Unknown PID: " + PID, decode_hex(data), ""] ) + return [] + + elif PID in feature_PIDs: + return decode_feature_pid(PID, data) + + else : + print " DONT KNOW how to interpret mode 05" + print " PID:", PID + # same as hex + #print " Raw:", data + print " Hex:", decode_hex(data) + print " Ints:", decode_ints(data) + # text needs to be filtered + #print " Text:", decode_text(data) + return [] + + +def decode_mode6_pid(PID, data): + """ Decode Mode6 . """ + + values = [] + + # pids/tids/mids are not necessarily in the CSV file + #if PID not in PIDs : + # print "Unknown PID, data: ", PID, data + # values.append( ["Unknown PID: " + PID, decode_hex(data), ""] ) + # return [] + # + #elif PID in feature_PIDs: + if PID in feature_PIDs: + #return [] + if len(data) == 5 : + # pop filler byte on old style messages + data.pop(0) + print "feat PID, data: ", PID, data + return decode_feature_pid(PID, data) + + else : + #PID = '06' + str.upper(P).rjust(2,'0') + print " DONT KNOW how to interpret mode 06" + print " PID:", PID + # same as hex + #print " Raw:", data + print " Hex:", decode_hex(data) + print " Ints:", decode_ints(data) + # text needs to be filtered + #print " Text:", decode_text(data) + return [] + + def decode_mode9_pid(PID, data): """ Decode Mode9 sensor reading using PIDs dict . """ values = [] + print "mode9 PID, data: ", PID, data + if PID not in PIDs : #print "Unknown PID, data: ", PID, data values.append( ["Unknown PID: " + PID, decode_hex(data), ""] ) return [] - elif PID == '0902' or PID == '0904' or PID == '090A': - # first databyte might be a count or something, - # should probably be handled per PID, - # ie. 0904 should have 16 bytes, 0902 -> 17, etc. - if data[0] == '01' : + elif PID in feature_PIDs: + if len(data) == 5: + # pop message count + data.pop(0) + return decode_feature_pid(PID, data) + + # message counts for next pids + elif PID == '0901' or PID == '0903' or PID == '0905' or PID == '0907' or PID == '0909': + values.append( [ PIDs[PID][1][0][0], decode_ints( data ), "" ] ) + return values + + # VIN + elif PID == '0902': + # 0902 should have 17 bytes + # VIN: XXXXXXXXXXXXXXXXX + if len(data) == 18: + # pop message count + data.pop(0) + # old style pads '0902' with 3 '00' bytes at front + if data[0] == '00' and data[1] == '00' and data[2] == '00': + data.pop(0) + data.pop(0) + data.pop(0) + values.append( [ PIDs[PID][1][0][0], decode_text( data ), "" ] ) + return values + + # Calibration ID + elif PID == '0904': + # 0904 should have 16 bytes + # CALID: XXXXXXXXXXXXXXXX + if len(data) == 17 or len(data) == 33: + # pop message count data.pop(0) values.append( [ PIDs[PID][1][0][0], decode_text( data ), "" ] ) return values elif PID == '0906': + # 4 hex bytes + # CVN: XX XX XX XX + if len(data) == 5: + # pop message count + data.pop(0) values.append( [ PIDs[PID][1][0][0], decode_hex( data ), "" ] ) return values elif PID == '0908': - # TODO: split result into C pairs of databytes - values.append( [ PIDs[PID][1][0][0], decode_ints( data ), "" ] ) - return values + # IPT: 16 values, each is a 2byte integer + #print "IPT decode...." + + if len(data) == 33: + # pop message count + data.pop(0) + + ipt_names = [ + "OBDCOND", + "IGNCNTR", + "CATCOMP1", + "CATCOND1", + "CATCOMP2", + "CATCOND2", + "O2SCOMP1", + "O2SCOND1", + "O2SCOMP2", + "O2SCOND2", + "EGRCOMP", + "EGRCOND", + "AIRCOMP", + "AIRCOND", + "EVAPCOMP", + "EVAPCOND", + ] + + while len(data) > 0: + A = 256 * int( data.pop(0), 16 ) + B = int( data.pop(0), 16 ) + #print "A+B", A, B, A+B + values.append( [ "IPT: " + ipt_names.pop(0), A+B , "" ] ) - elif PID == '0901' or '0903' or '0905' or '0907' or '0909': - values.append( [ PIDs[PID][1][0][0], decode_ints( data ), "" ] ) return values # insert new elif sections here + # 090A ... else: - return decode_generic_pid(PID, data) + return [] def decode_generic_pid(PID, data): @@ -934,11 +1052,11 @@ def store_info(self, rec): self.info[ecu] = {} if pid in feature_PIDs: - if rec['values'][ecu] == []: - self.suppPIDs.remove(fpid) - for pid in rec['values'][ecu]: - if pid not in self.suppPIDs : - self.suppPIDs.append(pid) + #if rec['values'][ecu] == []: + # self.suppPIDs.remove(pid) + for fpid in rec['values'][ecu]: + if fpid not in self.suppPIDs : + self.suppPIDs.append(fpid) self.suppPIDs.sort() if ecu not in self.obd2status: diff --git a/obd2_reader.py b/obd2_reader.py index deb7e75..20b4a9c 100644 --- a/obd2_reader.py +++ b/obd2_reader.py @@ -129,15 +129,23 @@ def connect(self): #try: self.Port.open() self.State = 1 - if self.debug > 1: - print "Trying to flush the recv buffer... (5 sec wait, min)" - self.flush_recv_buf() + time.sleep(0.5) + #self.SERIAL_SEND_cmd( ' ' ) + #self.flush_recv_buf() + self.SERIAL_FLUSH_buffers() + time.sleep(0.5) + self.SERIAL_SEND_cmd( ' ' ) + time.sleep(0.5) + self.SERIAL_FLUSH_buffers() if self.debug > 1: print "Trying to send reset command..." self.reset() + time.sleep(0.5) + # reset protocol to auto + self.reset_protocol() + time.sleep(0.5) + # report what protocol was discovered self.rtrv_attr() - #if self.attr['ProtoNum'] >= 6: - #self.Style = 'can' #except serial.SerialException as inst: # self.State = 0 # raise inst @@ -338,6 +346,31 @@ def triage_record(self, record): print " ERROR - BUFFER FULL - Increase speed of serial connection" #return [] rec += 1 + # "BUS BUSY", "CAN ERROR", ??? + + # if we get a 7F, that means there was an error + # 10 - general reject + # 11 - service not supported + # 12 - subfunction not supported OR invalid format + # 21 - busy repeat + # 22 - conditions or sequence not correct + # 78 - response pending + if record[1][0] == '7F': + mode = record[1][1] + err = record[1][2] + if err == 10: + print "General Error -- Mode:", mode + elif err == 11: + print "Service Not Supported Error -- Mode:", mode + elif err == 12: + print "Subfunction Not Supported or Invalid Format Error -- Mode:", mode + elif err == 21: + print "BUSY, Repeat -- Mode:", mode + elif err == 22: + print "Conditions or Sequence Not Correct -- Mode:", mode + elif err == 78: + print "Unknown Error -- Mode:", mode, " -- Error code:", err + return [] # format an OBD 2 command for further processing at a higher layer @@ -541,12 +574,11 @@ def format_obd2_record(self, record): # TODO elif self.Style == 'old': - print "OLD Style -- ISO/PWM/VPW .." + #print "OLD Style -- ISO/PWM/VPW .." if self.Headers == 1: - print "Headers ON" + #print "Headers ON" # trace of this to test is from 2006 acura tsx - # singleline and multiline are the same # >0906 # 48 6B 09 49 06 01 04 A6 FB 5A 0B # header: @@ -563,32 +595,28 @@ def format_obd2_record(self, record): ecuids[ecu]['data'] = [] if len(lines) > 1 : - #print " Multiline.. " - # 0: pri, 1: rcvr, 2: sndr, 3: mode, 4: pid, 5: linenum, 6-n: data + print "OLD style, with Headers, Multiline" + # 0: pri, 1: rcvr, 2: sndr, 3: mode, 4: pid, 5: linenum, 6->(n-1): data, lastbyte: checksum # add mode & pid once, skip linenums, concat data ecuids[ecu]['data'] = lines[0][3:5] for l in lines: - #print "adding line:", - #pprint.pprint(l[6:]) - ecuids[ecu]['data'].extend(l[6:]) + ecuids[ecu]['data'].extend(l[6:-1]) #print "data:", #pprint.pprint(ecuids[ecu]['data']) elif len(lines) == 1 : - print " Singleline.. " - # 0: pri, 1: rcvr, 2: sndr, 3: mode, 4: pid, 5: bytecount, 6-n: data + print "OLD style, with Headers, Singleline" + # some pids (mode 09) will have a message count in byte 5 + # needs to be filtered later + # 0: pri, 1: rcvr, 2: sndr, 3: mode, 4: pid, 5-(n-1): data, lastbyte: checksum # singleline ecuids[ecu]['count'] = lines[0][5] - ecuids[ecu]['data'].extend(lines[0][3:5]) - ecuids[ecu]['data'].extend(lines[0][6:]) + ecuids[ecu]['data'].extend(lines[0][3:-1]) else: # ERROR ! #print "ERROR, data record too short:" #pprint.pprint(record) raise self.ErrorIncompleteRecord("ERROR - Incomplete Response Record") - while ecuids[ecu]['data'][0] == '00' : - ecuids[ecu]['data'].pop(0) - elif self.Headers == 0: @@ -599,8 +627,8 @@ def format_obd2_record(self, record): # singleline vs multiline (with line #'s) lines = sorted(record[1:]) - # Since there are no headers, we will assume everything was from '7E8' - ecu = '7E8' + # Since there are no headers, we will assume everything was from '09' + ecu = '09' ecuids[ecu] = {} ecuids[ecu]['count'] = 0 ecuids[ecu]['data'] = [] @@ -652,6 +680,10 @@ def clear_attr(self): # fixme - consider SERIAL vs. FILE def rtrv_attr(self): """ Retrieves data attributes""" + # + if self.debug > 1: + print "Retrieving reader attributes..." + # if self.State != 1: print "Can't retrieve reader attributes, reader not connected" raise self.ErrorNotConnected("Can't retrieve reader attributes") @@ -664,6 +696,10 @@ def rtrv_attr(self): # fixme - consider SERIAL vs. FILE def reset(self): """ Resets device""" + # + if self.debug > 1: + print "Sending reset command..." + # if self.State != 1: print "Can't reset reader, reader not connected" raise self.ErrorNotConnected("Can't reset reader") @@ -673,17 +709,26 @@ def reset(self): else: raise self.ErrorReaderNotRecognized("Unknown OBD2 Reader device") + def reset_protocol(self): + """ Resets device communication protocol""" + # + if self.debug > 1: + print "Resetting communication protocol..." + # + if self.State != 1: + print "Can't reset protocol, reader not connected" + raise self.ErrorNotConnected("Can't reset reader") + else: + if self.Device == "ELM327": + self.ELM327_reset_protocol() + else: + raise self.ErrorReaderNotRecognized("Unknown OBD2 Reader device") + + # + # Plain serial functions (private) + # + # fixme - consider SERIAL vs. FILE - def flush_recv_buf(self): - """Internal use only: not a public interface""" - # after connecting, wait 5 secs for something to appear in the buffer - # if there is nothing, move on - # if there is something, read and discard it - #time.sleep(0.2) - time.sleep(5) - while self.Port.inWaiting() > 0: - tmp = self.Port.read(1) - time.sleep(0.1) @@ -702,6 +747,8 @@ def ELM327_rtrv_attr(self): def ELM327_reset(self): """ Resets device""" + # FYI - interpret_at_cmd can't handle an empty list + self.SEND_cmd("atz") # reset ELM327 firmware self.interpret_at_cmd( self.RTRV_record() ) @@ -710,11 +757,38 @@ def ELM327_reset(self): self.interpret_at_cmd( self.RTRV_record() ) + def ELM327_reset_protocol(self): + """ Resets device""" + # FYI - interpret_at_cmd can't handle an empty list + + self.SEND_cmd("atsp0") # reset protocol + #self.triage_record( self.RTRV_record() ) + self.RTRV_record() + + self.SEND_cmd("0100") # load something to determine the right protocol + self.RTRV_record() + + # just for good measure + self.SERIAL_FLUSH_buffers() + + # # SERIAL specific functions (private) # + def SERIAL_FLUSH_buffers(self): + """Internal use only: not a public interface""" + # + if self.debug > 1: + print "Trying to flush the recv buffer... (~2 sec wait)" + # + # wait 2 secs for something to appear in the buffer + time.sleep(2) + # flush both sides + self.Port.flushOutput() + self.Port.flushInput() + def SERIAL_SEND_cmd(self, cmd): """Private method for sending any CMD to a serial-connected reader device.""" # Must be connected & operational @@ -738,6 +812,13 @@ def SERIAL_RTRV_record(self): if self.State == 0: # a slightly more informative result might help return [] + # max seconds to wait for data + max_wait = 3 + # seconds to wait before trying again + try_wait = 0.1 + tries = max_wait / try_wait + # how much we have waited so far + waited = 0 # RECV raw_record = [] # raw_record is a list of non-empty strings, @@ -786,7 +867,12 @@ def SERIAL_RTRV_record(self): # wait a bit for the serial line to respond if self.debug > 1 : print "NO DATA TO READ!!" - time.sleep(0.1) + if waited < max_wait : + waited += try_wait + time.sleep(try_wait) + else: + self.recwaiting = 0 + return [] diff --git a/obd2_std_PIDs.csv b/obd2_std_PIDs.csv index 8f00ed2..820c70a 100644 --- a/obd2_std_PIDs.csv +++ b/obd2_std_PIDs.csv @@ -170,12 +170,12 @@ 5,210,,"O2 Sensor Monitor Bank 4 Sensor 4",0,1.275,Volts,"0.005 Lean to Rich sensor threshold voltage" 9,0,4,"mode 9 supported PIDs 01 to 20",,,,"" 9,01,1,"Message Count for VIN",,,,"" -9,02,,"Vehicle identification number (VIN)",,,,"HEX_to_ASCII" +9,02,17,"Vehicle identification number (VIN)",,,,"HEX_to_ASCII" 9,03,1,"Message Count for Calibration ID",,,,"" -9,04,,"Calibration ID",,,,"HEX_to_ASCII" +9,04,16,"Calibration ID",,,,"HEX_to_ASCII" 9,05,1,"Message Count for CVN",,,,"" -9,06,,"CVN (Calibration Verification Number)",,,,"HEX_to_ASCII" +9,06,4,"CVN (Calibration Verification Number)",,,,"HEX" 9,07,1,"Message Count for IPT",,,,"" -9,08,,"IPT spark ignition (In-use Performance Tracking)",,,,"HEX_to_ASCII" +9,08,32,"IPT spark ignition (In-use Performance Tracking)",,,,"PAIR_HEX_to_INT" 9,0A,,"ECU Name",,,,"HEX_to_ASCII" -9,0B,,"IPT compression ignition (In-use Performance Tracking)",,,,"HEX_to_ASCII" +9,0B,,"IPT compression ignition (In-use Performance Tracking)",,,,"PAIR_HEX_to_INT" diff --git a/scan-to-trace.py b/scan-to-trace.py index cb1a929..319b77a 100755 --- a/scan-to-trace.py +++ b/scan-to-trace.py @@ -79,9 +79,9 @@ def main(): 'xonxoff' : False, 'rtscts' : False, 'dsrdtr' : False, - 'timeout' : None, - 'interCharTimeout': None, + 'timeout' : 2, 'writeTimeout' : None + 'interCharTimeout': None, } # create serial port (closed) @@ -112,7 +112,7 @@ def main(): reader = obd2_reader.OBD2reader( TYPE, READER ) reader.Port = port reader.Headers = 1 - reader.debug = 1 + reader.debug = 2 # we want a record of what we pulled reader.record_trace()