From fb8817d659abfb5dce8334a9354011de7751600d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 14 Oct 2014 22:51:31 -0400 Subject: [PATCH 001/569] Catch OS no file error (for running on desktops) --- obd_utils.py | 66 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/obd_utils.py b/obd_utils.py index a6f72f65..9e8aadb1 100755 --- a/obd_utils.py +++ b/obd_utils.py @@ -1,32 +1,44 @@ import serial import platform +import errno -def scanSerial(): - """scan for available ports. return a list of serial names""" - available = [] - # Enable Bluetooh connection - for i in range(10): - try: - s = serial.Serial("/dev/rfcomm"+str(i)) - available.append( (str(s.port))) +# returns boolean for port availability +def tryPort(portStr): + try: + s = serial.Serial(portStr) s.close() # explicit close 'cause of delayed GC in java - except serial.SerialException: + return True + + except serial.SerialException: pass - # Enable USB connection - for i in range(256): - try: - s = serial.Serial("/dev/ttyUSB"+str(i)) - available.append(s.portstr) - s.close() # explicit close 'cause of delayed GC in java - except serial.SerialException: - pass - # Enable obdsim - #for i in range(256): - #try: #scan Simulator - #s = serial.Serial("/dev/pts/"+str(i)) - #available.append(s.portstr) - #s.close() # explicit close 'cause of delayed GC in java - #except serial.SerialException: - #pass - - return available \ No newline at end of file + except OSError, e: + if e.errno != errno.ENOENT: # permit "no such file or directory" errors + raise e + + return False + + +def scanSerial(): + """scan for available ports. return a list of serial names""" + available = [] + # Enable Bluetooh connection + for i in range(10): + portStr = "/dev/rfcomm%d" % i + if tryPort(portStr): + available.append(portStr) + + # Enable USB connection + for i in range(256): + portStr = "/dev/ttyUSB%d" % i + if tryPort(portStr): + available.append(portStr) + + # Enable obdsim + ''' + for i in range(256): + portStr = "/dev/pts/%d" % i + if tryPort(portStr): + available.append(portStr) + ''' + + return available From 89ca193ef6f7893369c4945aaffa1b98b85b2845 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 14 Oct 2014 23:04:29 -0400 Subject: [PATCH 002/569] added standard gitignore for python --- .gitignore | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..db4561ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ From 1ea6260263129cc95abc9e964d29f512e41430ef Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 14 Oct 2014 23:08:31 -0400 Subject: [PATCH 003/569] tweaked readme formatting --- README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 4acfc6ec..0bf48f50 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ pyobd ===== -
OBD-Pi: Raspberry Pi Displaying Car Diagnostics (OBD-II) Data On An Aftermarket Head Unit
+### OBD-Pi: Raspberry Pi Displaying Car Diagnostics (OBD-II) Data On An Aftermarket Head Unit
 
 In this tutorial you will learn how to connect your Raspberry Pi to a Bluetooth OBD-II adapter and display realtime engine data to your cars aftermarket head unit.
 
@@ -33,23 +33,23 @@ We'll be doing this from a console cable connection, but you can just as easily
 Note: For the following command line instructions, do not type the '#', that is only to indicate that it is a command to enter. 
 
 Before proceeding, run:
-#  sudo apt-get update
-#  sudo apt-get upgrade
-#  sudo apt-get autoremove
-#  sudo reboot
+	#  sudo apt-get update
+	#  sudo apt-get upgrade
+	#  sudo apt-get autoremove
+	#  sudo reboot
 
 Install these components using the command:
-#  sudo apt-get install python-serial
-#  sudo apt-get install bluetooth bluez-utils blueman
-#  sudo apt-get install python-wxgtk2.8 python-wxtools wx2.8-i18n libwxgtk2.8-dev
-#  sudo apt-get install git-core
-#  sudo reboot 
+	#  sudo apt-get install python-serial
+	#  sudo apt-get install bluetooth bluez-utils blueman
+	#  sudo apt-get install python-wxgtk2.8 python-wxtools wx2.8-i18n libwxgtk2.8-dev
+	#  sudo apt-get install git-core
+	#  sudo reboot 
 
 Next, download the OBD-Pi Software direct from GitHub (https://github.com/Pbartek/pyobd-pi.git)
 
 Or using the command:
-#  cd ~
-#  git clone https://github.com/Pbartek/pyobd-pi.git
+	#  cd ~
+	#  git clone https://github.com/Pbartek/pyobd-pi.git
 
 Vehicle Installation
 The vehicle installation is quite simple.
@@ -65,16 +65,16 @@ The vehicle installation is quite simple.
 5. Finally turn your key to the ON position and navigate your head unit to Auxiliary input.
 
 6. Enter your login credentials and run:
-#  startx
+	#  startx
 
 7. Launch BlueZ, the Bluetooth stack for Linux. Pair + Trust your ELM327 Bluetooth Adapter and Connect To: SPP Dev. You should see the Notification "Serial port connected to /dev/rfcomm0"
 
 Note: Click the Bluetooth icon, bottom right (Desktop) to configure your device. Right click on your Bluetooth device to bring up Connect To: SPP Dev.
 
 8. Open up Terminal and run:
-#  cd pyobd-pi
-#  sudo su
-#  python obd_gui.py
+	#  cd pyobd-pi
+	#  sudo su
+	#  python obd_gui.py
 
 Use the Left and Right arrow key to cycle through the gauge display.
 Note: Left and Right mouse click will also work
@@ -83,10 +83,10 @@ To exit the program just press Control and C or Alt and Esc.
 Update: 
 Data Logging
 If you would like to log your data run:
-#  cd pyobd-pi
-#  python obd_recorder.py
+	#  cd pyobd-pi
+	#  python obd_recorder.py
 
 The logged data file will be saved under: 
 /home/username/pyobd-pi/log/
 
-Enjoy and drive safe!
+Enjoy and drive safe! From d8a81577d5436f634a8fba25fa199d1a0171ba3f Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 14 Oct 2014 23:11:50 -0400 Subject: [PATCH 004/569] escaped # signs, blocked code --- README.md | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 0bf48f50..b5515c4c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ pyobd ===== -### OBD-Pi: Raspberry Pi Displaying Car Diagnostics (OBD-II) Data On An Aftermarket Head Unit +##### OBD-Pi: Raspberry Pi Displaying Car Diagnostics (OBD-II) Data On An Aftermarket Head Unit In this tutorial you will learn how to connect your Raspberry Pi to a Bluetooth OBD-II adapter and display realtime engine data to your cars aftermarket head unit. @@ -30,26 +30,29 @@ Before you start you will need a working install of Raspbian with network access We'll be doing this from a console cable connection, but you can just as easily do it from the direct HDMI/TV console or by SSH'ing in. Whatever gets you to a shell will work! -Note: For the following command line instructions, do not type the '#', that is only to indicate that it is a command to enter. +Note: For the following command line instructions, do not type the '\#', that is only to indicate that it is a command to enter. Before proceeding, run: - # sudo apt-get update - # sudo apt-get upgrade - # sudo apt-get autoremove - # sudo reboot + + \# sudo apt-get update + \# sudo apt-get upgrade + \# sudo apt-get autoremove + \# sudo reboot Install these components using the command: - # sudo apt-get install python-serial - # sudo apt-get install bluetooth bluez-utils blueman - # sudo apt-get install python-wxgtk2.8 python-wxtools wx2.8-i18n libwxgtk2.8-dev - # sudo apt-get install git-core - # sudo reboot + + \# sudo apt-get install python-serial + \# sudo apt-get install bluetooth bluez-utils blueman + \# sudo apt-get install python-wxgtk2.8 python-wxtools wx2.8-i18n libwxgtk2.8-dev + \# sudo apt-get install git-core + \# sudo reboot Next, download the OBD-Pi Software direct from GitHub (https://github.com/Pbartek/pyobd-pi.git) Or using the command: - # cd ~ - # git clone https://github.com/Pbartek/pyobd-pi.git + + \# cd ~ + \# git clone https://github.com/Pbartek/pyobd-pi.git Vehicle Installation The vehicle installation is quite simple. @@ -65,26 +68,32 @@ The vehicle installation is quite simple. 5. Finally turn your key to the ON position and navigate your head unit to Auxiliary input. 6. Enter your login credentials and run: - # startx + + \# startx 7. Launch BlueZ, the Bluetooth stack for Linux. Pair + Trust your ELM327 Bluetooth Adapter and Connect To: SPP Dev. You should see the Notification "Serial port connected to /dev/rfcomm0" Note: Click the Bluetooth icon, bottom right (Desktop) to configure your device. Right click on your Bluetooth device to bring up Connect To: SPP Dev. 8. Open up Terminal and run: - # cd pyobd-pi - # sudo su - # python obd_gui.py + + \# cd pyobd-pi + \# sudo su + \# python obd_gui.py Use the Left and Right arrow key to cycle through the gauge display. Note: Left and Right mouse click will also work To exit the program just press Control and C or Alt and Esc. + Update: + Data Logging + If you would like to log your data run: - # cd pyobd-pi - # python obd_recorder.py + + \# cd pyobd-pi + \# python obd_recorder.py The logged data file will be saved under: /home/username/pyobd-pi/log/ From 532c7fcdae935063cacd2af6dce307f802b2edbf Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 14 Oct 2014 23:14:13 -0400 Subject: [PATCH 005/569] more readme tweaks --- README.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index b5515c4c..43e3f0a6 100644 --- a/README.md +++ b/README.md @@ -34,25 +34,25 @@ Note: For the following command line instructions, do not type the '\#', that is Before proceeding, run: - \# sudo apt-get update - \# sudo apt-get upgrade - \# sudo apt-get autoremove - \# sudo reboot + # sudo apt-get update + # sudo apt-get upgrade + # sudo apt-get autoremove + # sudo reboot Install these components using the command: - \# sudo apt-get install python-serial - \# sudo apt-get install bluetooth bluez-utils blueman - \# sudo apt-get install python-wxgtk2.8 python-wxtools wx2.8-i18n libwxgtk2.8-dev - \# sudo apt-get install git-core - \# sudo reboot + # sudo apt-get install python-serial + # sudo apt-get install bluetooth bluez-utils blueman + # sudo apt-get install python-wxgtk2.8 python-wxtools wx2.8-i18n libwxgtk2.8-dev + # sudo apt-get install git-core + # sudo reboot Next, download the OBD-Pi Software direct from GitHub (https://github.com/Pbartek/pyobd-pi.git) Or using the command: - \# cd ~ - \# git clone https://github.com/Pbartek/pyobd-pi.git + # cd ~ + # git clone https://github.com/Pbartek/pyobd-pi.git Vehicle Installation The vehicle installation is quite simple. @@ -69,7 +69,7 @@ The vehicle installation is quite simple. 6. Enter your login credentials and run: - \# startx + \# startx 7. Launch BlueZ, the Bluetooth stack for Linux. Pair + Trust your ELM327 Bluetooth Adapter and Connect To: SPP Dev. You should see the Notification "Serial port connected to /dev/rfcomm0" @@ -77,9 +77,9 @@ Note: Click the Bluetooth icon, bottom right (Desktop) to configure your device. 8. Open up Terminal and run: - \# cd pyobd-pi - \# sudo su - \# python obd_gui.py + \# cd pyobd-pi + \# sudo su + \# python obd_gui.py Use the Left and Right arrow key to cycle through the gauge display. Note: Left and Right mouse click will also work @@ -92,8 +92,8 @@ Data Logging If you would like to log your data run: - \# cd pyobd-pi - \# python obd_recorder.py + # cd pyobd-pi + # python obd_recorder.py The logged data file will be saved under: /home/username/pyobd-pi/log/ From c48692462def08ab9423914c84b1a5aa01da07c0 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 14 Oct 2014 23:15:45 -0400 Subject: [PATCH 006/569] more readme tweaks --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 43e3f0a6..318bcf1e 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ pyobd In this tutorial you will learn how to connect your Raspberry Pi to a Bluetooth OBD-II adapter and display realtime engine data to your cars aftermarket head unit. Hardware Required: + 1. Raspberry Pi 2. Aftermarket head unit (Note: Must support Auxiliary input) 3. Plugable USB Bluetooth 4.0 Low Energy Micro Adapter @@ -69,7 +70,7 @@ The vehicle installation is quite simple. 6. Enter your login credentials and run: - \# startx + # startx 7. Launch BlueZ, the Bluetooth stack for Linux. Pair + Trust your ELM327 Bluetooth Adapter and Connect To: SPP Dev. You should see the Notification "Serial port connected to /dev/rfcomm0" @@ -77,9 +78,9 @@ Note: Click the Bluetooth icon, bottom right (Desktop) to configure your device. 8. Open up Terminal and run: - \# cd pyobd-pi - \# sudo su - \# python obd_gui.py + # cd pyobd-pi + # sudo su + # python obd_gui.py Use the Left and Right arrow key to cycle through the gauge display. Note: Left and Right mouse click will also work From 3039e2c0b0859ccf1686547534d4f26e53457b5a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 14 Oct 2014 23:25:20 -0400 Subject: [PATCH 007/569] deleted GUI and debian packaging files, since this is about to be overhauled --- bg_black.jpg | Bin 50040 -> 0 bytes debian/changelog | 5 - debian/compat | 1 - debian/control | 13 - debian/copyright | 45 --- debian/dirs | 3 - debian/docs | 0 debian/rules | 97 ----- doc/install.html | 55 --- doc/mac_super_screenshot.png | Bin 131755 -> 0 bytes doc/super_screenshot.bmp | Bin 2359350 -> 0 bytes doc/super_screenshot.tiff | Bin 206340 -> 0 bytes doc/x11_super_screenshot.png | Bin 244058 -> 0 bytes log/LogHere.rtf | 7 - obd_gui.py | 590 ------------------------------ pyobd | 676 ----------------------------------- pyobd.desktop | 10 - pyobd.gif | Bin 1288 -> 0 bytes 18 files changed, 1502 deletions(-) delete mode 100755 bg_black.jpg delete mode 100755 debian/changelog delete mode 100755 debian/compat delete mode 100755 debian/control delete mode 100755 debian/copyright delete mode 100755 debian/dirs delete mode 100755 debian/docs delete mode 100755 debian/rules delete mode 100755 doc/install.html delete mode 100755 doc/mac_super_screenshot.png delete mode 100755 doc/super_screenshot.bmp delete mode 100755 doc/super_screenshot.tiff delete mode 100755 doc/x11_super_screenshot.png delete mode 100644 log/LogHere.rtf delete mode 100755 obd_gui.py delete mode 100755 pyobd delete mode 100755 pyobd.desktop delete mode 100755 pyobd.gif diff --git a/bg_black.jpg b/bg_black.jpg deleted file mode 100755 index 95a790aaad7f6ab03bc8a2042f05ca6f3214800e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50040 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 - - -- SeCons Ltd. www.obdtester.com Fri, 11 Sep 2009 15:24:35 +0200 diff --git a/debian/compat b/debian/compat deleted file mode 100755 index 7f8f011e..00000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -7 diff --git a/debian/control b/debian/control deleted file mode 100755 index d0b44eff..00000000 --- a/debian/control +++ /dev/null @@ -1,13 +0,0 @@ -Source: pyobd -Section:utils -Priority: optional -Maintainer: SeCons Ltd. -Build-Depends: debhelper (>= 7) -Standards-Version: 3.8.0 -Homepage: - -Package: pyobd -Architecture: any -Depends: python,python-serial,python-wxgtk2.6 -Description:pyOBD is an OBD-II (SAE-J1979) compliant scantool software written entirely in Python. It is meant to interface with the low cost ELM 32x devices such as ELM-USB. - pyOBD was written by Donour Sizemore, now maintained and improved by SECONS Ltd.i and it is Free Software and is distributed under the terms of the GPL. For Python devlopers, pyOBD provides a single module, obd_io, that allows high level control over sensor data and diagnostic trouble code managment. The entire package has been tested to work on both Mac OSX 10.3 (panther) and Gentoo Linux. Generally speaking, any Posix-type system meeting the requirements below will be supported. In theory, Windows is also supported but has not been tested. diff --git a/debian/copyright b/debian/copyright deleted file mode 100755 index 8ec6535f..00000000 --- a/debian/copyright +++ /dev/null @@ -1,45 +0,0 @@ -This package was debianized by: - - SeCons Ltd. on Fri, 11 Sep 2009 15:24:35 +0200 - -It was downloaded from: - - - -Upstream Author(s): - - SeCons Ltd. - -Copyright: - - Copyright 2004 Donour Sizemore (donour@uchicago.edu) - Copyright 2009 Secons Ltd. (www.obdtester.com) - -License: - - pyOBD package is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - pyOBD package is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this package; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - -On Debian systems, the complete text of the GNU General -Public License can be found in `/usr/share/common-licenses/GPL'. - -The Debian packaging is: - - Copyright C) 2009, SeCons Ltd. - -and is licensed under the GPL, see above. - - -# Please also look if there are files or directories which have a -# different copyright/license attached and list them here. diff --git a/debian/dirs b/debian/dirs deleted file mode 100755 index 1994a9f4..00000000 --- a/debian/dirs +++ /dev/null @@ -1,3 +0,0 @@ -usr/bin/ -usr/share/applications -usr/share/pyobd diff --git a/debian/docs b/debian/docs deleted file mode 100755 index e69de29b..00000000 diff --git a/debian/rules b/debian/rules deleted file mode 100755 index 04373fbe..00000000 --- a/debian/rules +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/make -f -# -*- makefile -*- -# Sample debian/rules that uses debhelper. -# This file was originally written by Joey Hess and Craig Small. -# As a special exception, when this file is copied by dh-make into a -# dh-make output file, you may use that output file without restriction. -# This special exception was added by Craig Small in version 0.37 of dh-make. - -# Uncomment this to turn on verbose mode. -#export DH_VERBOSE=1 - - - - - -configure: configure-stamp - -configure-stamp: - dh_testdir - # Add here commands to configure the package. - - touch configure-stamp - - -build: build-stamp - -build-stamp: configure-stamp - dh_testdir - touch build-stamp - -clean: - dh_testdir - dh_testroot - rm -f build-stamp configure-stamp - dh_clean - -install: build - dh_testdir - dh_testroot - dh_prep - dh_installdirs - - #Create temp dir - mkdir -p $(CURDIR)/debian/pyobd - - #Install program files - - cp debugEvent.py $(CURDIR)/debian/pyobd/usr/share/pyobd/debugEvent.py - cp obd_io.py $(CURDIR)/debian/pyobd/usr/share/pyobd/obd_io.py - cp obd_sensors.py $(CURDIR)/debian/pyobd/usr/share/pyobd/obd_sensors.py - cp obd2_codes.py $(CURDIR)/debian/pyobd/usr/share/pyobd/obd2_codes.py - cp pyobd $(CURDIR)/debian/pyobd/usr/share/pyobd/pyobd - cp pyobd.gif $(CURDIR)/debian/pyobd/usr/share/pyobd/pyobd.gif - - #Install menufile - cp pyobd.desktop $(CURDIR)/debian/pyobd/usr/share/applications/pyobd.desktop - cp -d pyobdlink $(CURDIR)/debian/pyobd/usr/bin/pyobd - - #TODO:Install man files - -# Build architecture-independent files here. -binary-indep: install -# We have nothing to do by default. - -# Build architecture-dependent files here. -binary-arch: install - dh_testdir - dh_testroot - dh_installchangelogs - dh_installdocs - dh_installexamples -# dh_install -# dh_installmenu -# dh_installdebconf -# dh_installlogrotate -# dh_installemacsen -# dh_installpam -# dh_installmime -# dh_python -# dh_installinit -# dh_installcron -# dh_installinfo - dh_installman - dh_link - dh_strip - dh_compress - dh_fixperms -# dh_perl -# dh_makeshlibs - dh_installdeb - dh_shlibdeps - dh_gencontrol - dh_md5sums - dh_builddeb - -binary: binary-indep binary-arch -.PHONY: build clean binary-indep binary-arch binary install configure diff --git a/doc/install.html b/doc/install.html deleted file mode 100755 index 5eef27d7..00000000 --- a/doc/install.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - -

Installation Instructions

- -

Prerequisites:

- -First, install these and make sure they work. - -

Python:

- -A recent release is needed. Developement took place using the 2.3.x -release series. Any 2.X release should work, but hasn't been -tested. Under MacOSX 10.3 (panther), Python is installed by -default. Installation instructions are available at the python -website: www.python.org - -

PySerial

- -This is needed to communicate with the serial port. It is available -from sourceforge: http://pyserial.sourceforge.net/. Download -and install it. Use version 2.0. - -

WxPython (optional):

- -WxPython is needed if you want to use the pretty graphical interface -to sensor data and DTC management. It's available at www.wxpython.org. Version -2.4 is required. pyOBD will not work with the 2.5 -release series. - -

Ncurses (optional):

- -In addition to wxpython, there is a text/ncurses interface. To use -it, enable the ncurses module when you build python. It isn't very -polished and use is not recommended. - - -

Installation:

- -After those requirements, installation is a snap. Simply download the -release tarball and uncompress it. To "install" pyOBD on the system, simply copy -the release directory to wherever you want (i.e. /opt, /usr/local). - -To use the wx interface run python wxgui.py. If you're using -MacOSX, substitute "pythonw" for "python". - -
--- Donour Sizemore - - diff --git a/doc/mac_super_screenshot.png b/doc/mac_super_screenshot.png deleted file mode 100755 index 006080d779ab9d7fed29d1a821f5ba275337dfa6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 131755 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 z z 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

fe8{>Mq5 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 JRNo~3|35>+_sswR diff --git a/doc/super_screenshot.tiff b/doc/super_screenshot.tiff deleted file mode 100755 index 2ad6a4d4a0fa4b79f6f887bf4ea61457b3eba14b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 206340 zcmaHy^;Z;3poZzL1r~N;m+p{~?(XjHuBAgjmPR@xq(uajlulV1>F!vXj}|Efx!&LJ zx&Oh;nK?7>%=12)nkXD7=nVUjD0Dg^pecgz7#@1)wZ=6@?uMNQfj!m@CLTuO%sG?J&1RlPU5>4r-t87%#^M!A=Rw_S z^y;IscLV}aHZUa%MrnfEL5XW^h?6ihZPdZnRN}K?>V(UO=|r@X9)b;Tk#C)nEV-JM z3vK~sJx*VP{nwUf)Xe)=M|dZ{HOLmPl7FpWi=4ytV#*bpQ0yM?!#! z+Lz8nh>jr90AeE2J)qeAB+1OU{pmH__yZ&pf`kL$5Iy4d4ArUX3P&X+BL~i~(Uy0f!)d&2Qa{q7(Cjm-Jd$3kOo# zuQBdcYe8bV*{}f>-R5vcb-m^y9!>q%70lZDdgqC`Xx6Fv1nCo55g)!TF%$o_n`>ii)P6p@%ImQQUQF`tLZ5h{|yjfWexCz+~ zYWWjp7R3D=xt_;87Hvs+h#QT})53-!SFq^AZG%%@l6Koav&Py7hmR!LiyTU)ID}AO zCOWj=X(!uvDE{z2#8z^Azb|S8$aKp0GR;0%?bR%CTJiEJb?&5iTkax?74YG>(S7f-abLzhn_+SB{L-x^QeKhs*LJU-G^ zmi+uH9lv@JVO{_9yzTcGseOJaqkN58N`UjW0S&Wvh3bWN{tiB)_<|6@#wd-UjB0Sg zlvM#^mo`JAbs_-$AU;%E3G7&2W(fh*IkvKBeke&MK;Fa{9jQPa&pSumk0Y`_d?pJ7 z5J(K_Q~(g!s_56RSQOG70sYyE#C!w>gvYgnWeXfhyx+J75?Y8E&ZOdDA9(vs-h%B0 zZBleI1&4#CR)@13vEV<&h}q607zabrba#eF+HYy?e}(a7V#yMNaU_^9;CvrF^pbdY z=`c#CsN}5_Mou_s_PgJv`;uyma9 zZ*t(#>_ZB%ENtQF2f{QvMuaQ5b_=rtVN)T~V$S}j1p*%ewb|Dtou1B$vp$AvRo_S) z{kke4$BWRx-IG4*!7LN#jhG3=l}C^1{ot?Vu50>UvFbf2k6bTlc1=LJYLhC@;GO;K z4{6mCLb^}>Uoz)Zv((rvc`KT~;a8Ny+<0A>&{=bM1_9EK@W}oda>o;d4C%gYN1kSKeUqVZ}Dmwod(6ac8&T4#;g? za@$xf2=YEF>TbPbKw6l?>Gvf9X>@0#@RJI~FJeNV%Ub)&U8UmtlFMDfN^+JyeIoI6 zIphrO!q`X#`k2yBpmHvkRMdV^Y*J-VG@A>qj~o|+TGxeXUO zoENkMmfO(TnbnLQk^&$;#^@)}2MdnSV5hB6V%UDIjl5d9gxJvm(`C_ku(2S6BB7FY zHPT6pQ?G{FqbYX?JmHPq!O&Cos zXUJ~%*A)99>_Vhc_)J)bbft(l8QQUXs_rHYHtZNup6p8C;}1O=HXm==<2k7AD)LJW zn(}+Y6FW=5iKQ|DvI7ak6I@F-OIb~ajPp9zQkOMbrB1EMD<6m1r|=c?rh#C-1lL{+ z=;=0i2oqkc(tVnk;4+H6Xw8!pp3m`d{ELs)8mF4hse;4MrL{H_ckYn7+k{i9M@Bpc zRPC4{ks1nl4Cj>Oq#77#1JpmNxpVfm@f#K~`HDK$yUC(vUYs+8U5q4Ftzy+Jn;Wql zqrp2wbX>qi-SY1vP8GlkzhXieSqMkbB_Fc6_46O9M*$lf-BHGCHWW-dI`2zv1R^-F z7x`zN%e{`hHn_KKcf&jMdWv%-1e!amJcOMa|_mTLd$Yxd=Z@py+f*?xCZwx5;j zlS}(Xa4~5gbMM-?@u&Xs(Ym!u+vmb^x--b?eDhBg^3vkLCunwZd+&B0 zy7}a1JH;gSr1)|S5d*7TcdUyoQ+DnO|3G`;vWY6mCyns1eViRT8ksKpWfkTdG8=z5 zSdx$Dgg=gU4LyAl&N$5*ZzPzpJ>M6pzn9tD!hZ>qlEU`<2~}`^(yhGSiywKk=GlMp zX}kIT_S-`+8fB>0DBH01>aaDQBFyk)Gns>UU*4E>r4s$ON>L4&W_f= zjdsPMkj{tB2tbFiqw7dwpdN$Zncy8A9F%S>x@>~JGZF+pDMF0`Hu|L?^h)$AaSUED8q^sos90gPy+;T=IMn;}EVqk~g`;A6Nm+r%Va3DWP;Vf{4e zFC@G-cmQlXU3|Rs8k({>5J?NZ9TqN}7m9ief#cA@v5Bs@p!C_Kav0c%L{ePH|x6$;+x}uLCG$AE59#sO>SWParMQj+oCL zIO|LSumDlZ8Q2B3>P_S+$p)3gi!A6EM3+5{X%8pRl+7at!P-LWmea0ZO*5Jkw zDBn0ZV;$&K`s6;EBpE+x;4@%)US2vA0+}U3n>@@Y97{)T+zd+_QT zf;$T3SE!^-I@@a+oZIMrByYO{1r#lA(8dQRS&N4vUWw7C${~~%@0~K^Pqv8<;%u2i z4Bx5=zNzwLjMB}6Yv!0mawb^yT_-xdWlE;m^gD_u!j-aS*|S4EUlr+A863R+U8`1a z@P3IC79<%yYa7k(kz*)WOlaLAdLp!np0m@~VX+cqWVm%5(qna*yFNgMQiHIf=Lr)8 zAQMsSHiB2wrsiFuN zL1eR8DEiCJUJ(wkcnJOZqM#v#t-t_L0u9!~NHvGplQ`U(JwfOonFNwtDioilB@BKU zhd>GCDt{N|Z}QFYM5PDasbko_`U4DOiFOozYV3(e9CUMKN1SRuzff|kyGV3#sb`iA z@MDx+wQ>^<;zrnmhiyN4fU!cp*ykUX#W+$a`uN&XD6YOUryLg5GNUYq{Z)$p7+mA< z>IMy}JfXKp|7tG#1BC-ALl+ALahz(aXYLRfi$Xy5n!-+x|Cv)2dMFFj0ytZm_>-a8 zvnYa`#fSzM^(J`?SAb9T>L%ImQ*2f#AU%!~LYjGS8fVjZ^GKj*qEp#O$dpgP5qK#c z@@t%e78I{o;%qJ?SsH3QN>as}8dcgWiWiM?6tdNpa`VQTvH}@m6>Iw~Ot`YzMAAmM z_Cg}#Q~GT9_I=M|0y60KHZmP6|8AQ)FAf-yg2~>#RT9gKOtOyFC6OT+wCPFQeyIKP z&Hs%Bc#sSjD!T@qE7z6&f=*{JZMpC0^ubSL_^O zCHT#*gWSxWJhs&x{5fU1?q}x_PaovAWmM%qb7ldz#9U$SJ~wAso{mh3O1`GkKi1~` zx9e}M+dO+Jy;tug+X9+{pxbY^?u&Ba$OkD_`+fHR_a;xz;cuFwrt4s9?8cj?x7w5EZ(h6y&Yw$o(*8y3`o%wIvBY;oZ_7_#++(YB zWL|$1O--NWDwo5QRzA!v6)BFw@o*{>VaK#-{QkN;0ta{B+Lp(TTYol)Re@zEG#Bzw#x))oYG{YuPWI%6{H zl`>jgg>6EWYSNellO&Wc+_dPk6mSC*e1I7Tp?o;G@`83^Y0a3TOX|G}IUW_ZktkG- zfZM*Y4wA9&tlVHqs~Gk+r9R`K25w1j%ftmkfZvAr=O@6Ac85Fs$O9VwAzrL87u=M-N6DuIaJgAAAkW=>GE+mWd(GndK zb$)$RCC^ zu3*(R8h9&K7y+79xTT1oGKjSudOR z3n)gWVrQ=~V`n4?#~ZqQL{)f&^@p6g#xNPbm49euorc^aRyyMR^I^OUAAyyMy@;I!xPQk?gmjtF|ESYIL#4CcpCglYGma< zGUSGEocmiZW!@6PCAot{WQkuaTPD!nBbay~LJC`_l?;t^+?11vIwr4&ItXJs&1X(% z7`6djZg7tKqd^_rUJg1fzJQ>v@DEw;HBpGoS*a`g*Muh&p)E+Pkf7iV@lHY(3%&7d zQ9y(s8oQOK_tHc&P~{0=oh zf09O&G_PFPr3YN@ADA{rrL5{I$zI{#nmbSBtm)b#TIqjRvOpED>n5rCiHMDTk@y!8 z+Ovt+Z2*Zzbd1)iq3UQ&kiGn|Lo}MT)Vr_hxOHBz@7d?ndwBF0I{;O@-d_CIR z->7>(Iy2=)b0v8$hdKN`^wG%-D|ZQ$u^+KNdU+%%{}f_h-d~#Uffm; zMj6fAub2}o6iHU$&L=L?pQ}w`sno1yQw->AK1{_@&pp1eAPN1ox8tOu7%1B3-ie!; z+P~y8k}=l7Os9!0Ir2^mu)gVxwFS*e>r7pp-1>7FSlIL12cxf-D(ct|fOc$~jBw+{ zQfUPVJXsUG!{Ccm`9|3vW0(YIVvU6bk?cBf#f|AP4CWjT31lHc>rqd|kKngGr%SVD zsdoK3S+|1Qj7wsh=L9p>V5v$l!K{qKfTlmVd6H5e7NX_*5&(Jq5_Xy1$B(UDPu(xp zXvDiqBd@8)FKeK&j2OD>Eq)-N#qN9q9{TS&xBhnX<$>sI5CwH7l2Sic}*kSfrVm>&=t+-;5 z=peYHWtoHreP^TnNh;CrWxDZYM%^(ZB~m~V7!Q^L1)cHgEXjE?m?$QgI;zo0SJew@ z@h-6Myt7eN5*Q*f{ID_fQM0)Do_HOXSb+rLVm~6sPJ^!~i+gV0@`8((ND4h(3zd2t=h7@_A zibdh5LD8s3u|jv8>Qj;GS=kSQ64k+SMgL%yr+^c`+0 zHHC`4!CJM#IyHkgYG(~<{`G3zO==V0)M}c?$s2w+w5V;gj&rrEJ$0yCG^-DGs!Q~! zJG6~Aw~zno8;5)!uR*B$BPL#VOfdCKSoBUPj7&(3eoxOgY#@lYy`zA-I*~Z7as&q`gfl$Wk+kFbT(>ZcQsrqgDu}*v7b6x-zD0k&b~P z!;12P-o4vnUXyH|qs6ovRd|?s%#(H#5PMdOuLmVf9@0?zoKk8%wZo!yHHKnv71exQ zrlMY&JwGXitIc&C)qEAHGp2RLqP1h4PToHe1rvGw)Xo9J;pNr7;?O~V%Lb#gb*J6*pxG1`)ASXM_m!@k z@efrUtW9#XUCB7Vo~{*i-BKmp=75S)A?5}$Runzmt1)3xKa_L&PnkZ^lZ>7A_iZEw zb2bO)Z;Vib?e((mr({sj6**&KxjHw7CC%$PZx*8~c$41~iL*@*HsZpwk18QQKZEGe znT5L&ATxp_Bj(*|Dv^xLAPXT(ym-Ve?7=4cSD!cIIo1URQC5--bAYT8v84p#V_?dIb5gI+38hasE&>~D%}dAG?4T02^i z7R`rLCxE3?4X1%1l!xU<=T{@ySJVxHu2a1zy01B5tnS?0M$Dg}x*}xxT&q`kxc4$FKdSSIdFjR^CDDf3c7;A1z;-L77>SRH8fc z4A!TbQ8;I03#-FMZ|P&pM5_a^C@3A$C*IA9C#Z)wy{W`ooXia@P*!EnO|_&cQLkP; z4Git+{iqwZ{q`Zuit^n#f82kRnI6+*h|;Pv8q;ZQd}KrLSt;0B`46&6pi5g-%0sFG zW$Hpq9Cj?l6H?VFDt{qwQ6zU!Mjl{fOQUq%D|Cvynv-fs`Nsk|MdVq({7 z@-7kuN6>KUeBoRR9u=tzaFEOb1@FM8)Q_Ik=ZkLjZpSMp25jGpDpG7brU@?0jE1P9 z&jM}=c`D!^wg8UcoFC0cNesNz`_Z8#h0JIDl>|)4Tpm7BRZMP6S)^G7ky#q69%gX7uN=nPE8hPeBuL{K~I<5)T zRyvz4DgJ{5M(5R@>q5^x9u}0`BVHE9nqz(&!j_Bvmdh!~kj=J=$)K&a$;%LHW5@e9 z)+>b9Z@1g2qHK^Thrn2*-7q9JJ1cgUf#UT}nCx?EKKh(MGiQsEWL9w(^kmscN5WxKkjxHeW6Q>7-z z=1^6A{Tlsy3I;umC+dzV;|M8g+{vS>Z>rvXwP1Lj@3q->9&5G0FeWZF?u|t~b=z}U z@E_?ls1QdT(QUfAMC=1P#MA;7dJQ#Jc0Be~9vt{m=2ah?)K11d{YRnhk0PuP^eeYl ztMZiYF<@~W>)xXBkp1|175@B%R5=Lmtx8ZeeNY(B14Z8^GV(7Q=3a)C>iWS?(T9Vh z1giw#NmN92`03WJaWn8uu_`%T7z*nkPgsI=GcysbD%oumtv5kTf^?$5WAb)Yz@KXq z{jXX~_UQB|GSmq<+|nxs zjN>qXU8GdPtQ@VX&A&(h6T-e``uAY1g2Mn+eR)7a8x4U-os$NcCXx~$uOuGw*gW}q z3?-gJn*$MJK$P)=NA^S;uiwLeFqSu)G+UAeJ(HPyw_n7DJw5@I%O+KJhcBjC2GjP~ zbs&l$#|{OBSk+`zaR!xg9W@?r2w^u=h9oT8$4;4j=cFJUB#O*(GM6P!G{9c%C(fr!-wbDqP~^v46RL%r}1(9WzRGw1};l z=oF(}RYCOghshNBZ;m5KJN~@NUt)M2^+AF@5!w(j#muCHR`xR4o=#}m^}V3;s1-5V zneRvYVQFl-1(5B7z?7=1jMJ}&52S0Bv+S5G`%G2kfNofFBL~&TL}PBoPrd}UoNN<1 ziAmf_uT*1R2oKP*P*AyFDFKojel$oDpqW?@;$ZQ`gyb9Y86~BGNh+|&{^}hvp`g#k z0&Qo`xjt(w$Mex~DjlDHCU>h-aTBKfgmMhxVENd`NMyb!=TpOwQQh|Ay~ zAy^@#QxOdQ3Iq8j>bud!nQTHB1N#Zb32owzYl#iw_>~-a%-Che0E|%(RsS-U^6D1S zfsbuDdnR98!@hjI&FNbDqiV^`U0yHrVR7xty_GH;pTfcM1$qr+@gH7=yZ?D?b&Rnn4jii$HU~H9mV{G@`6*<2#_DX_KF* zl6?l){^+wavN+&ly^{FhUhK#~kQc5c1jRoxZO#M|;A)|COurK_ufY|Q*dw98GE^4n zQB>%-ODYy1)3j(>!oD%dAqJ;uTry*emV!~&ZX64HAT4y=*Hs?3RbZ}rwtJCW*f}lj zgbVb!PSUV|g4<76k;D&yp2mbNcf+f&b%Q|9&p~ZMSEn1)!BC8y392%f7jeIdKzs%h zB!;b!c=xq#0zCu!AX^p?4Wjn0_JJdYB$K>sK>;@dhgsoxjXFBW<3h4!pxtX}eM8>E zM|O!zE$qA<B67)NT zdH2Kiyso8~2mVL{&bsW&0wOaRv54K)A*VIy2JFa@ z>ku7+YA>xL&}#`91v4WoRvvrgVLy0k%m3NyJrOd^#Dc|pC?P-Nfp@jWHk`jpvzP9g zRL#UQOfq~&GirfXI&!E$GFen^A@o?!esZ|-Lh~_Q=y7NDin!_%QLpzt?e_blp6Rap zUJbE#za+%4cunuO!rV02Uw%FHy&)UGaZe+l2_9M5CMogW3)$Y>@1of~moR=jkAU#S z+_RoR{`BDTpb()l0c8!y)A0IiEt2bekGviLiEP&we^F3^k0zU`k;P<40zwo69=fW7 z$@V>>jpGZtlc_{B`Y_nkFF@+U(Iw4a_$Y{8X^)qmqtAdC`T2UX|1U4>Z*A+^49y-q z^e=rXsaGlcRP*7*=rMihMFZ3Nl*3_EA*NOZHq7YnOaRW6C*H*@9xM<-0z^XvT@rDl zPxT#N{(eEST&3fY+o}MOG1uvu_;qKOL|=)BF4hq-q&8p9P<@EB5Abzr@p--R8vc;* zK9j&2t{YKFE>B1r`~Vr4fJR(WJ|JEOl(dl>{&g#}ar#KmA3fZ!JAo0m6=3V{L_Z1r zYzMmD*&SH|aLJR$y@o^;Q~TVKAw+3E7O(SNupf3qxa3^npVp&_7x7f=^+*>1Nnk)2 zl`j(+Dckfo0}t0T1DuaRwvzH2%{tcr6U!Y8al};11UU1+iyoxRGzmk2gMB&}XU)ie zy09a&SkC)rql#US`gpQ}tk-|flBcle5*d{b$@m2sWxuogIs%V&ap>$h_&z|1;~yMB#iZ8l!qd}YKXU>%hLX%}up_Hq@u|4bWiZbjy0E}t zeQ$W2r-*%@@cd9R$7zl07NNfrTZ2l6^ z{RH%#0Z=7K%GiLi5HedEQhEsrSvGQd7gB^A1%i!S)`b*Rf&!I*j9!m;l#ZO<4um=k zDVx|3U|A=NTf0f(tK}5YnP)P+7#+jWL^{U8_3JOp5})S zQrzsHBcmY5Y_h3$rv?-2QzxjfNBOMLi{^7p%@!@0sVh``ocVlQq`tgo14Q=S%T(V5 zHUTvv3gGo9y&$RSj|HSdV$p>V2k^+W$jaQxou_B%UhK)(PJxk@(kLhd$ZW_WpUA@R z%j_5Z^E$BhE$*ePc7TVom%M_ZvevDl32f^WB@hr0qNy_^I+Y4RWRo@jN7(`wGk{z) z)5|Y@)iy+N4iLnms{+vv83|Tu0*A~4#!OygTawFKw7fAfoHlvCX(c#s1w696J-0Gk zwlo~E2Hsn>JX$QiShhUd1l(BV;9K3{TDKD0ycV}@p0_cgv=u_NGorUO;Kgj(iETN{Zx1d2Me%G)eS+TLl~QV2Ta7~AF;Itj@-87Vrkfme^t~w|F9O`p`%(rDW!8fi)1W<~GNsna(Js`~y0hLf6 z`}J?KYz6kSU09#7C$|BGmmFlbco7-oB~K`gUH0)ItP#tW2X6q4l!q6U&7jVKXvz&sET z5gBD0Nmgb;3Fjq8dI1nJz`ivA!i1zu3oLvc6RaB3b^$7bq}df?mqDl>G$Ul~h}7gb z`hHT-w?!aEV$sd;`sRq~ z5b*`{m%YOdr25Bj8ESB4eo`PZUEzhGG7Zu)2nGEo$irqt=|}uT$IQr&_#K>CqAWS4 zd|4q9_;9=__!Shj3T&%Jh7ch^FaZr_(cqmyu3o+syw0sx}Z^WTzBFlnNA_}Qc z!w4U_GvR8~U<)c_C=gBnu%*mAX;O(^qehJ*wPhmRnaqsb{(%0w&{Uw{by7jT6iKtU zs%dN%JrziH$^m}0R{h&CakH@WBO$_s8o36s4Fn>z03F73dxJD(YDBAonXBS?A%dkL z0+}K(($X62(m4P^4g@Cp2g*TZ9E2iHM6xDSCKKdzeH?v#08|a|a2}XGl7bsTA##&o za>fC=jtJi2039XBW|E79l7-Sy$rgf8H4{s-NjvqZZSA;rLWyKOz!OqndeicDBZ`$D z(1-i9PW=ekDNyN80+gRXcu@Lp7Qov36p`mLDnv}D6?%5<*5Z()Ns) z4XS~!fnGRj zukkUs;E{DTHexxC{m%n9Psw3t0C65iEWsRT!xm(G0M(>Jq1zj< zZ5rsx9?_PEwy#k>sVp7^4H9PJSr|t6Tn4M-P`4dYbUUC*xKhF6f}A+R#0Mfg^9V{E zM}CqAIo09S`bA9a+M-5B*uEcgwxAvf8vA@WqZd}{HeGcH5P8vj|6 z(%p+ow>?oAgf>r^lO;9W!mB;9NFjI zH%x?bMhyRVX|j-)xsf5QC=pr|W$%FWFfgj&T$uqSeL9nE)*L)78a06o)r6964uEta zqz6$zE@vdp7Yk1pH}0p~kWwAv0R$f} z`n*Ad`k8Rca-xhEv@aoH+e`Gwz9-8?CA(8PFh~nGA}ZY?Tx}^?L*47mdypz5qEQ3C zZYbLF*(>%XqYr&5JEkeSphf+UN)|?e`VRU!n`ldguuKDrn?swQPHrWCY9)Cr<+s6R#s=ucPV+P>sNnM@zQbNGS?j%Y)m^4Y1gXL7x!`(g@u1=C0`U zvIlkdg?%|Mcb!8AuXOK5TKT?h^XE3->UQicqL~sYgV)O>*q2R6&vsG*zbxkd({u9# zy?@fMftU-V&_i#($3ZtecZxcuzLQ-$YF}>=LAS6Iw+NSt=2vZu=rGET(Qq+Q@Fv-n z)_b-t;^mQ7qaJefr0*1MGlG+yUSA|B+h11`SSgcd4m0?&?SIi{dOu$A%{AxL7V2x`_4d-MqZ7A^e%;+Hc?(*RA6RPLZ^~?ts_~ zux*Xo@V0iGMvF8|bNLfUl5wh5%)8!F6&bxxtTl396{n1w+-pG`6wDYKUC^9wi#-rU z05?~Me6Jts#-g+G^C~JkQU2x-SLc5Zs6Bf%`_j&F8T2Rq?>t_;`DEZLuJ1Xs1B)`*G2pa=<4W*Ac7aXfU)!^aE>@bnCB4F$sQJ;z668QZ#TpZYPJ5T)X z1Kzt#gz`HEa-@SUY;Vb^`Q$=f6kXT@D4F&R>@bIICET!v4IfXV&N0gUY-(iwyPd-sX21qcGFffF_u36s$oo z@nid9AfX)Wn~uA_ri~J7M0Ythf){MjJg;Ac~$P5+an( z0fbwvn0%8=EnfvBrW*Gggc9Z@9E+xjw44Un3r`uQAG8}HGpyx_WOHm8aa96oACWuG z1i`&Vg4Ul%)>D)1iDI)4#jN=99orXPoa4SvBLb6tVMpTAe$UwQv;N_%>aOXQ^300?PC@2VRGVq4&q>-{e!4w~9_G(z9 zfpUP&i~#U~X^5?jDC)I7kwFN?0)#ReK*S*1&PYlZqn055kwGFj1#@_`l_ZV#RTA<$ zIfK9QJ6>OIL{%55J%M_%j}YtNXcFb_FX!aO&q7L7Ll8zL-{e_ev47uu)O!mdQCveo zF~fxu4(M{}dYF|eB2Qfo3UP#bujEv!!L<9y=^5vE$}0bw0v|{`$oE2uiQ&CMSm|Tn zJ(pmCF&)8Jmy`^CA=Muz0?f=FsVM+A$zhyJ?(+Tioa;Kv@skjqz2u%eKRnS1)il2H z8bp2;nzM#!!E2VVu@3@zt`i;gG%N`l0Cf@+v@#qa7K60{{{P%26D!1=n*S>zs|(Uv zMT)XI{4Bbe3!ZWv=XPH3EFqhUm^OWpJlg0e^<#i*|G*bHCb%oheig5iKP~SP^H@f1 z6s7A%s8BV8_tEcn>I|N=>~Rlc`D>|^St?Ccwgc9W*H2l( zOP$i0A3h%lIw09Zyqj!qmZThVU33^#pysv--0c=IwdYOF8#w zt+Pg<0BWYTFAXAAZZnAA`2~$mw#1{S4ZkwHL*(wBZFtc7zS>;qGEq80x|heomPiJI z)d?)a#AC0Czyo=?#C8(0iH#H-gLx$sc7>M7K34p-_Q|F=4O`$rwmL}gG_Bpn+hl&i zjiE;}DhDojrmvFv=qfRugGEWkPcO9}Ap&gsr};Tv#b*5>e#8eBi1$A~+x=L5L+%8b z%ofnno5(NcK78sb0%r#x)H|#bY(j@LA@sjU%5i})6DS8aq^v66*q6`&Q zOqzHu{e_h|V@54;oP1l+bogOT%umiGoc&_}R`G;)#MHlP@#g@VuZvV!YX40N*Sx;? zHvhZL%vI7d&)w3qHlF;!{Tbd6NHMTdKxyxx_=8f?PkZ@Y&8FqtnfTwji&*g5b6UkF7V$Aw1mYJ;J@ew+jY^ zE{o2;-;bbgTi{1rNfGu5Z{r~)$lk*0@CQZS5+Nb#vDaGEh=*y^e?{sTufMXRA(V{! zg}GV25xPG@j5!Yq+j4#X+D?Sz3LF+L7x+c^Ooc3q9~Iu0`2B^?gi^{M7g3h`M`X-} z8mpfaWd9?nqBdG8Ou!LrxSXwyiGdK9FlJzv$Rt~ilPF4I$0u*WiV~7I@{u2>Ig@uk zMUiRtQ6bQnF=y2Ax+6Kqoi%&(b5?%g7jLd&?L%@Zrp{RLa$P5bkXsHqH4gBYAgQ8W zxlXlV@~yt4UFE-Q@YQe}7&YlwXUZY?N7HKAzRk#E>FfK}F7J*F&m8KZwb`)F9iG59 z7kJ1Zf%!`ELPs8;%b+0yd<@f)7b@cSU^($Y! z3V4X1xz`^W`FSVtRIi=3yma?l{JYf4_m*EbVc{id#-}@fC8uvLe)nlJp*+f@i{E1) z(k94X5cOxYsECbJ4Q=31tbrx@`wRz1kdm&0 zE7cId0A@IRJ@|%dc=1iC&3sBbr*}r$ZKl>d)8ZOv*m{1;8=i!FtFKU;nRM z>J64rKM`7H)Ml7Yr>8 z4VDB;eb_ZyCXkkDzylx472tWa^@78S_v&Q0Wf%{tE&{r4p z!1rBlniRJKFwJ}5xUZI9@=1VYPD@kJ`rYD4h{b)ye2|rJ>`}N?+rg!mbX~o#qgNrJlO;=tQ4= z0JhT3ISh$#%yY^g_Dpr^5DF-9`b`v4aoPa8m0Cls& zYO4M3!(mI^8P(sRdXEC4@y4@tt=X2dKVFM%o&^yH?H2`c=N%V+>aKe(%`a}gUlw9L z_gxjz5+ZycJY)mc7T(~I>##KFm@lM+`^OEWQgHkxY+7{sW|LTa_7{{@aqbo>Z8#rL zq;0-<`+quDm;c(KRy@&r55>wU`;W!eMF*iY-sMNXt?H{!f7v;;oMZML^_)M^Tn=15 zS@U$9hK3zmvEdXSM}hG^DkC2wq)X0!ztC3R{&{&j{qVB=3-xUS5sHioCaR4BHe81a zOD2yRZO0!FMWTg`Fk!|l{X`k9^Y1qyK@w}{2^vF^3Xn6A$3-%+#ms!BkZZTZD~sWd z$l~(4d_Z>2~D(BD{FEAgb8( zd|Labh;&jSSrt=s`hBK^^f?wKl@2JwrZwFvF}KT=1xsOfz(To|1=q=-H%JaU%UnJ-}Z;{ZTevsQwx z8ndFy5M{Z6S*q>_ai!y1`cLGkX>(?Zs@2S_6$YPk^xj#jorH6LCZEdJH?!9~sSx?R za9FJ08LssOQ?lyy-+#_kqV9iC)v85~PX?Vu`fjlrUks(H7pbZZ|7$V*(&ANP_@l+x z9n-qTF!Y-d)ws#2pldC#u4!p)(fq5Ucb(yIr!n5S#c5AKJ@BH(cl>;F6pOCX(W57)iNb5rbX~yt?4Y!1DQ?b8T+^jiv7$w& zc+qU#)cxGDv1RGUin+L_r{_>}o6*+hdO?Wy`RzZFMzmw09_!=oU74xU;)l5*~TE^fvOpAmhLb+N=P`$dyDv&O05(gQ8` zk4v62`)Arg*(-jv=FfF{Yq(AdRt8{|ESSB~bAzf^-3nD>!?QX5YxqJeFJrRwZzZF5 z<|qPM+n0VHS-JOw*R^8xtcbnXdRQhl-qDP$taCbe^b|D(#!al5OS_&!t6T0`SJ%3{ zJv=8{+JdmQH_YGodO^qA?}bh_sq6hNHW$0voG!P_M?$>`&%5t?Z?~y0!@W0ezTY3A zY;Ryk!Tv>sc4bVY1Z^yA3q0^(4M1+FC-_Kl4+W2q?^#%XM)+BZpMELl$u=WQg%F-ENKbcT3!ER?Q_04 z-X0`%zWHXv|I%{1=f3!H`_Dc6vX{6oSQ`@=rW}85MLir`LcRMG^iQ(U{-;VYA9Pp# z8;bK(NSfr~(@W7WnzY$aYx$FB)~a7yl}n-DE&uC@uf3D{zWT=cU%V~pxZ672c=J8{ zG6K8%!RiJ1M4Nc7f)h!iIkW=skAd+Zkh}>7Y;iDN`CUGoY$JBc#9_PHszEPFXu2|! z?js~ll{HJvp)up}Q?nRv_VCb0mx&rxFc6n4f8)Op=@K31W-(0CSjA7eCkAegECuS7 z25vyHaH9&NFNUXv9}et#%o(om&Ao*Fyk9yA>e%vH{yKE= z9NuTmYy3omjT=8;2W#(E5&))*+W#~;s}S>0&Chbe6m@me?peC``@RJe-k@P62Ifv2-gNwR_*zh!o^A_eGmRo%N(lJ9 zFOaO8C;rVLjVm!;82E#CFL+6*%3UPQR7KH0+RkW$e_J|@gg(b&vwkGv!F<@&Rn#N*WK^CrsAX55 z^Pl9_)FWH?bpg;s`agf3yfcL-j3U+ZbU44e$GTz-EZZoNW5 z7;C$d!<}NkN}>7QaW%)Y$a#$-rNZ@pID5;mC$~>(_Bs3i{CTgr-ur!?yPlQwsw(?clJQ2z)%4R2 z=QRL!zw25C`}n(c;#&(JZPY4#FD=V7)#q5cG`Y7}d`g$>tYwCi#ERv;uU&>7v?6z9c(p1w z>#XC;2?b7n?MXJrc>O7b@_gfIHq0J&MiIZ;dX`;%+IIdAd)V3Yw+l*KpJjJO)ParWd9d4^zxJ^4cGuqRc5 zzb7su6ms!5Oa``q(@jR$I+^AWeTLH_c09WoNr(-PDPpM0qUKho{~${#-VqyEyE ziSvEGo!;*o>bHxZc`wE<172{>UO$__ax>3?2{4Z21!q|*mMpU!J9-$I8=K+M^#?_C zec?!K_)SUlAXN^Ow@Xxro1yr@s>&E3T^Rq-M;4;$gacZ7jS5Ag4ONXN5Z6^ghf1=9 zsaBJSFBxG%L%G9MVL5JHM{H;xK)a$E9#>G{cUa65dJvpISl<;X9gSh68UkDwzcoB~ zOf6bXnN>=+ig>F}FGkIYLu#o5u#E(cRV}BgVD`h^_$f|P&Lz^mzfZAI>H3~_jVILR zkgBlKD?x3CC)DMFtnlVT!WD|RtS~HTEg6=?LoFq{Oh&(Bn3OalFDoa^&S)X({eJU? z5Bb9sE#lA-Rbd?hO(Ywi<=f&kaSUygD5ICVron0AsWu9_8muU>_31pFy7I%;?9!pl zX*|{1qg~T%mOqq0>@C596?+3C^6QHc`<_L*Ea5%zHPZukzRkI*>vyP*Zh} z=dR-$sM&P8kPn~wK`(h=h+Z)F))~i_SLP6zHtgEXSsCt{38D8sfxuW^eML`Ur!NT| zeusBf#9oP|QTQ{_S@!IThf3M*n^G;5PnsBUszJ)&%TMc9Sn5}#`PS&r z`sux_@~Ys(=+QO62A@bpInbBd#j4pb^lBy5X-mpQbHf~g6c&`dMB3<39;V6*Nc~o< zHYPNcUq-`}LV`$F?+Ik%NSse()@6tZEfGZJ^`Sy^u|S8C0Hv8%n%O<)W8i^6sr(fx z64pJ#)!cVyVbe4?Lqs^}$RQvoUZy4o8HQ)?`ZOR^NfMTsyemGVbR2w8dV z9Oa5f4jZ%Pom!2)=$w#{Qb3|BRe%}`?&H-9AJr-VSU`<{X*72M;wZjR;zo~JlM7O^ z=Ok5#CRa8oGhsnPkK*WQdW>6~jBTR>X4OQBg^nmFm=kofG6Q#K8dA%(J!*h-*CxPp;FkmT-|21QniK}gnhM? za(I%LiaI3y7h{4ry_Z;a?vk~A!c-DC7AcH!ZWh^0a#x|Bc5|iGbfuak30-WnQM&WZ z(&HlMnjuDm%lTcJ;|*S^6Z%1-6ccaY+D&48Z9R zBS^(9ugJ&_zN(6=MyYKo~8Dv-< zP>FhJ;(6zxVByIQJ-n@<#LmMyuMR%9&H@I0+W#t@P;9eeKKFoDnBNx{Q&7_c%!vSv zC4CgxlKEnPNLE~n=SRz5Pig>aZlqkb2&+8JR%-&E`Em2t;rZbg*Esv_UALXJfx=o& zBLvoaoL)g9zMefzJPz8g-i_9uzA|2=b$QYu>_5qIe(_%%`K1$ssTWLXlr3^OW1HM@NokNZ zOjc*~ga~GJc+7fSawaWeN>t4bWKws3TL0bj?y2*)zkJr9itTe?;QrI^HxKY|rbLJz z5=ju>dgGmBZC)~%C%%g}wIS}2eT5aIO4d!=Cmca7!+H~s<>7WQPO5P4JUimlD^}Qah@jN2kYXW`ScfL-a zIGcyoDPGKe_eH=n8vKM#W>ORYaNeDKW$j88e$nHD;9~MdP>;rl&DE+hFgFE@V# zA2AV(3aw-xY1V$Zry2(dUa`iC%I60rE@!1^+ZA`$@`V>okztWoq0KlP0ad?bUke#1 z4L9p1DFattUfth>BNpVw9vwBQX~N;Xrd<9!!;xA)1=V2>Nr&AO+j^)|mA(`7_ZQ zjQgBR8uV=eUxkm^`l+}?b?kRN@9_Hj&E*Tc770Ik;Wy|tIs8Kta0x0;(Zy^7KTG!+TR!4pQhdltsk2qyeG=S{DXH8Nc&8k7Zt&85H7{+z zTjg<==U8(8W3PIX_VTU4@j*kiHbRx|qYS}eqnK-Ns@_225);ap167C>3z`hF>LBSX zU@|yLFUgpPA;gptQPOA{kjN;7mV@7er-8-lBFc7(=PSWx8U4g{UR5(oS)1w>2M)6> zO)LSJi@fHCj_@D{NH@{_kih9~;;a(YAPhnOITa*Mi;ULU%*p2%<4f#$GM??eU1KkX z;k*9@853YmWHbx6M2nIuL6zR3VO+q<6<)LJfam}=d1_O4ocd0N&t(?(HA0o8X8Nmk zjtoX^SP(rwf5H-TRD4o8+gagVqma*}rp|kA>035;0QJuI3dpp;zZY@xTchC#CrH-F+VM3Bj1S%+JEtp|%2!FaSJ@$K6W_>(9$LO+n+3m`_?thS}kjUTrC?5NRkFAYfuWuguQ4=pt*c`90I2p26_ z#OJVBE23|6_)q2YGb`ycV#Lnn^D&LJZX>uC=g^nT3s*_!DHYGy%t=>Khy6pcElxMr z&YiTrA*UkgFwN8btgXQRr$)=zS*LCH?m4i_)J0bm2*8f$Gjmq9Tv41);HOEo54Ts! zYEYr6!`$mF$ZwF^o_1KIobDMJCr`sFSw(-H0AiyQ^z3=$O^|D#oMqzK77vDf9kC6+ zCWzaq@EV+zrD(av^E0gYwQ=OBLyQ_-IGj8|HnTHhKI=Fc8pQiIz5&MHtee-*AN=2& zz-qJ>cvzEBia$ATCcf*!yqZ$4z|_mHsu~g5EMbZvcZFwqDfgkWD*RK+7ZwbJUX*GV z-zWI468(mziVAMs;7>p&(@$#&a0`jP{wRFEl{tOVNQ zoQ1qBJ)SKi;q3r$2m)%8N>6t>Ly9leSft~iC98FaYCQU+m9_o{(pFg(?gDkGrH+(L z=;an9SfR80ZIwQrHnIcTkyiEfdM$>ESWzw(BNIbFs2s0J``+B>0Z;qgbCYakPMiQ%hB#B6cR zqOnLDGURn;erc<)@Qn;&=(4uZ4kX%cWUnlGJ7s{Sr7aixo#`@XJvcy6zCLzeq(L)i zT5m)vsKq%uTr-VBNo{OpFb(obxh%*i1C=S?cUiZ2RlFG)#h!{e{4TkPio_8so9zUhdE~mPOGN_P|G}D zg#uNBj1SyN=)~*yu_k)yNjaV@Pr35q>3N&pl-a#sAFbKpyaC`?2b7eeo(C z@dfqm>7aS8=yg5hBGH`1~9iK1hnXlQM~w??iB$Am&LJq@Kj~UnyPL z7y)dkH#LfCB{o8r0yV*zvBGiy=k8`&rUZ@|wr%XJ4AJmg#2ojrQv8T_7l^ zi&s1Je1(FSQxsIA7q7M?7>3PI*x>IC`}DE)F)|SK`UYbsdCB&grjhrS7^`Io(1$H8 zXN1&FX^>lR9MN2So<}IqB5Qbsvea2@Hf9^|y7>xaTp2r)^{%CpG{a!`#nPLEEPDUmoc4@M9TU@2(c{J`)c`+CS( zMeg+V{@7Dqp;_P*IkClOq^*zj$z#esk0RDL$Jqk!!INaYqWiEwUHy?e_Tl_nCd;s9 z?X&MnU*qKL4Ohv{zUQMsKWo}zQ(&j{7~{Zy;;hzs$;#huuY*6sS)SiXm4a|Q=c}YX z&^SV}ZB?Pr7N~g6zf>Lk<7#vzK)!T$u*_W2DB4a(zOVEPY2$#gj!1~=?(=@zZH>cx zxttE?EfmECaucYZUO9UILG*unWO%sWzJWgt`K{N%pR%>-h4hkhSOu zSr3;77NlBV!PMrq)TLrd0w68`8Z9{SKBD@{VG{pL^~rc@&zYV=-iB63{H7_?VEm;- zybRK#Ev}yz|M96J)dNAHr_seohiwnk&cXG(yn;Bn!nQUdp_)wL~Wyj_j%&muN1wP5Q!~w&Q-_aYGvm5C#`z7A(Yx4#lyyOVTN65U85g=P|K4 zH811OR+-6K)KsnFEg#s+o&CvUd~s@7)RO*r=BI&{Z6|L}V<7gNlYL~|cc!Z54el{N zH^+Fj!gMzr&h}SLNAo|VY6fyTDLfDov>Y_t!qAr2JlGsJ9J0MR0k2nmcy1Ir6O@}4 znESN~iYEh)Q=6%z(5co7gH8xV2KF*!n$3>j&i~xdTkUYh z^4P#`><@J7qC3mEpM1;ge~U?3=wM-)q{t(j(Ry!u{$1hZ7eT$U|0G+?t9_NY%lFbA zk0#WbKSdvKdTF%o_mE5i)ckH&86M{lN6&CgI_?MapwQXfl4p}~&zSSQA6xe)v*+UYPFVpWEWvK}0F86{@ z1azBv&}$k%mxTK<)VoWBz~wyM(L#Ma#PHFH=5R5lbKW6+!aGj!iBuRaWf(P87#B5S z>_5<~Yc}78&wl#(+j;5tm$l#Lo4+{seicmpI-kVito$9+^1FK#4iAaCW)|+tDct!b zcDMht^9mL21{@bM4s-*qy8|At9-r=~E3LFiPYZKU8Y-SUfeT+OGi6NBQWR3DvBd}N zLK@}runkF&uFy^q03Dx@!HW>>eUyy`dXT7kcaI;aA*oOu52Q<0v_PRo zM_P~%4jQ8f%JNpt@`f%^(q<8d(5P6yryMkl#Mh?^Y9;GC0a($vcln4wYvNk74as=SJ3*h?# zWoYy)21rXj>0EA#aO(k}p*--&Bn36#WTCh#pssz%Jhlg45; zc7L9a@g;Op1;Xl_!kUo5T30iEo&%FVewf2KgYDVqV)HonsISJ^uBO>0JE`23a!|ff z@N9Ci!SW>9CuW3?L zY4AWy#8UD+L7l9j1=RR@7*cc;I}KV>qJr?DCc83xs-pN{UO>&3aP1+Vr2;=l*9^~; z%VL=*X`2jG!N1cNQBN;;9VEmXCS(r9bosjJ(#?fZ$i|DzQIF1Xjm>tA&kb_qPTm!E zJ0MR!p?+5=qSC>hLdaPU;M{uT;zgk%#C%o%SyZD!G$o3Ki&g;5O7@+;Yfy;@ybF0@ zDHSM{1Q|oA+x1Of4+$EV9K1i_EGr%e3Ez^994|kLS?UdQL*h^}keSseMVi1o1kqwa zo^V$J6ebbkO4?OyLNG+T1B{7u{EfR)Ufjtp^%|61B2$tQhpPI zUG&@;ybB9KL_A=rVnK5%P~-vS3_6fYG~Vov3cf{8(-pBB$-|Nq=*}r|BM@JED}%^D zLZl~Nn@$cNz%y4UH8d-a)C=5Ble*qec0VxZ$@#(a=^^=xid&URUA@ZO1HF0^=FHbA z3<@}GJ^(S1ZJGDe)x{HZTb<<&kjA{_0$1RHHU;8-bpF8Gq;#jj>jztwTL0R`g z4?L>N@1=KJpwC~d?@^{-IVv>x8FK=oK#Pa8QBb1*s&S30O)N>7Ca2lGs|CNSr6sJD z_F8KmWSC~D>0v`U?@++sW^lW1$PYCHA8YelYc|?z-d34bIl)@%a?)~2ng}52Bvg~6 z=WM{GXm&G9POxpW83{QA7LH*#p-LCYZz6}Jzg_gigMcS3%F*?Z5G^=5EEUGhPlzqR zvsM^Xu#sO6JV4oGZ#f)bHY#U59tp9aNWQTvxs_Z`uskCa8C_lCM1)6>Z51l$D59n;2?LG+tdDH|?9z z@9#D2-#qR{bk2&k%&J{$x6l~S3=P-=+b0yh?8Vd;!~xvj{OmRlEU*l``%Wr^>~d?y zt#j!JxB=Wh1}?!n=pvB{p{omFs|#KSI#&c1m4ED$ccwl&@GybI5QQk)E8qH90ec~)*7vA-veNM z9oF@S{TOjFMe4SiUoHRVPVNGqx8d9@Nntxz6eXv6pVS-$Ayw56myMhMNhY}Jw*T$q zHrr(=f?mukfCj2it zxqHvZH(NPEC6(}`{(C9a{kZ6CMEt7V87YirC>be^b_iEQKBdn3XD-tSL2NP0NOEP> ze=nsHD$etdr#P$&P7ucK3Qwd|p1ztS9J_rznR4>{W{LI9df*Rpg~8n8sg6`K zGemKWaxH!%wcmG={Rh)gAB2MxJ$@+gtsQ{&UkM(plN3SeEw1eOG z4CDj9T)=Q^K#tDgcwp`l`eD%b2eL!|T;ehRaa~f!hzQ-xaK?z(D~%aOqm~K&xb4id z*9kjhI8sSFnWvZpXkN&5`dOcU>19~9!mMF?kZ?!H-ea|EE=)>&m$#pV5|Dq8MIBdQ zOU{u}c&P6L`F!Zpq)>L0Rb5qaO!(+r^&J?t@B88v`fcsUHg zoV*8AYb`KpOQ__!r`8h$*kAt zyVGkF=-17sx5D27b5`@DecT84EnTQOs*lE;o)(@vy^PL%Ot;nmub>YfIpenfk_odp zVYtKkKfggde;DUg9Y0Y|NK9rJp%*TGe-`*2@G~4R_U@UgY&FDj|9pK`Qzp>a*lT?Q z<`1KK8t;;`l}f(LM}q&Lii7Z07aM-TZ^DujWdB+hA5qT%_pg+yd2~;G{l&d1sTA4J^2Z_8g5i=^w3Y*BDmZ{rSZB1UFD~Ulb4#Rjo}iAvM|o_ z|1Zou{u5^YaF&jOIm$4Y*|`gNldo!{DZ)tdKfnxfXdPTjjyND?$T`N69po(ZkLv&L z`9(}wkz`IVo4Im>^C8KhEVXubsr3az&|1@ib&x~Vv3C9L`% z1>vfrc%KES8@Ug^$b&BmeC9(XofK+8`FSNZAWIw#j;6L;fku(HxV6hmvbYT%kb;u@ zAs}nZ-`J{Ol{|cQXht!OaX8rl1~V9~UMyKW6`nqVpK}^iM?)*yw-V2UNubGt7Gv*~ zaD09qJ*ja#a#xh#tHnH(%8o5DP4p+E@)u^XZA9>q z;MUpj!h9qg=7_JHdQj!F!cR$O7O-2Ji!4ja9m^V zQu+%gq=vkNw<6B!I^k&+e#vZy1Qv|@t)YroV>E^;5EAYk^+5-kR_fx$GH#<9ZN>t< ztnClMyu_A9SzSp$d!B$ClHwsaUC*)Tz>j;a@=U9{hdS6bWIwW#7>Z=g z*q{AA0C=p`)P?>5Gonu;maA^I@~4M!DPy4}T3m{fo`$O#@wWH;N-6G}DHC#7&tfXV z|8#8RPNB0ZKbZeePAfiyv67#U$m5I4$`nXH5TON4=>pM+Rv;L&qm?gJd(pwwD)U7# zJUMxmEt_4D3{hQvc^(B}+OhpnUFp1jLG-#C;jK=RXZX>&E{>aU&<;(nXK(ea!^T87 zq7qP%amvXy;*icD$68kc6Ov|U-CMX8`+k|$E`vpQS0^Vbr8P~rbT*RPxh%D1F(57x$mZPcn10)?y6fz_ zb=`X;8^qe>Ij`UUdJ%JO$r1;O=SL9Y+A0wAeSUC z!(2ezAVE8L={LE!6acwfy9&82EW)N1K#z#mK~>>`)!ii;k)~_+GlPQ5JvjE$HRq9@vJIY+%_gJ`H>;=Y2ngBBGA$o zECAjG{4eSs1P|*F z-avbVcS8}V3K#Twnenns)fTMr_;W!l0mzm=NiunHOi?_e^P>!lAR4ePEZVczqu!se z3Q~DPyD|x_Y826Vot%`GI|TQUpg9`xZpwP&!uyhkxf=c`%F|@8_Cqf}YK(IOS)ec* z>f^`Rk$_3z=Qjt)@cH6ga;jIGPFu*pS`6UrITH)JkKix6|mX)tu1A z_f|400!Y9=7=PKqV(?+SK#dzvNPNILhL;2=l(nzpbrVEo0|7BTl6iija-r}}+G);G z0^!K{eWGoKN>}%JOux#1PyXe?Bw*pD`y#_sI)0Kci{fRYe?dA zo#KLFS8pSy6NaXKAn+;3W0j{b3Pj%g`Xs^-OBGRfhxS*oe+%GpAMP(H>l}ukx7lfdizXnG--r zja6tnX`LS&L|))U_I+|CLDGei#&FRk4V|dj%}0u@p(Y|_8v0lW8>{ZO4bS!HRvILD zt@MopIhfX`_TS07z;UpyCfv8l<^g$G^uBntwK_!=!J(flTkwxE!r`g2F|~{3oPCio zR$}PO3?8a@vsTK&kNig4t?HQJ_Bgm-%1hV8WwDJfM8BbrL;Gku_?vImwDSXn=4&UH z`XZ-`r`fL$zkPVsc`V|Ee`nLgB(W`dOsx4>eXW?=N2*?E?y0G{T2nTAL6hLr>^FY) z6xg+S(c-VnNs%zLbJglU;ruFZ>fi?EB>rOXWhsH<$&urD^qZ1jN4rzrOXrCVScbuA z!+!Sr)5Et;=PwoC&iGy?M_7rMp*aIW!E+V_s7W7@gEnpGFoW}#fVd&FXd?Jwv_kp> z;k3F|fbfDPw-@1Cq`q{Kw54bkf&KdzTJI%4TXDwHlaBDl(d(`V#?xn@Uy6+ogceg3 z5xPkT?dXvmCo@1;6;c?MMO0E5$>cTC81?j3JV%@34Kf&)eN8ffWYOjjpkC(PYb!|E z`D>ikuX@b;q*o%jOnNI`ADJLWetGzP=t!W%P!=ptc?_~eCpCsgHG|R>erEm!`k_Ud zOblUtYWapsT>g>9rg%JQOL$H5gm>Mw7U=z++i#QH#D%cLAk$j;jt27O?Xi4Z!4NI8RD@2{R(HiPi4 zPu8pD$88*|U(ee)DMqe4C})oD`J6NSuS7X#t$y=z5eg#mb7|P43UOsLoTg8X3Ezt4 zz3XI;-pc$)KR{<_$1up9<3%x8M;SstcscZuC3#tO1EmHV>&o?hde)DsQVomYI1x~_@8yk^pN2_`(%$zWo&CRdwwQwm-gEax~+SYJEUh*&+#dg^^ zE3MNNDz40FY&5L@=N+lPJFB|XE&^#^iS(a&H6FNc)@XOx3fozElg9tW_ zeAfjVK61CJ10ia6VkiatTiol1v;VjwCA##&6{7_t8K$Uc*^knoS^3BQOOjsnbwr+c zFS*Vx3q@hcJge3};KdLDRv7J2GFu$|5GT5mr`+FRk{~wII<3!cM2-A2?T}0Mz;d!N z(I_{X8mSw2!ety4edt{_3b)kDY=XI2ekvw#J2)|h1x*Eja&ru%Vxn!gg8;dU-i@f+ zJp;2-ZB1k3fqpFQmzFMuN{C* zGB#DaBl}C7_XNJqn9xBuYl^edRkJS5;M0%6k={pXCx@RHzdT&RC$aQhNwc*={@Q;p z8@By7g_LP8hu;0&S#!=`Pu@^rWIK^qSS!kTATw#|d4!!C-jgok7m)Cy%+0voBbm61 z+{1EQUJUoa67NlPzQt`lQZuD(KV)3Vr}R7niVuH$EDw}>kp$*E9aaIqH}eOe_Fw+Y zBa1X7W?-EF5lV4enE6|AtIM(#1*w}GA{U5BCG3DCf1DF_(=_m#jhRXi$4n=1pZow#zYE*k9q3k9zZA2}0*u4cgzcG2@5 z5A@@kM>leQ?{0+ZsSC!Ef6f*XC{hi}JUR&HMIUJ?1vi#UTz3?H??W+Zh%rrEQapqK)S1-zEVd}YYB8&F2ed3ZwzjWamP;4bLd(i0aw&LF1 z93ih8h%PZ zS@^-cr{>qoa7VI&ZFfuMsb4II#PpxO+-Su5@+%x9>ryp{d6L7mOsVDIt~I@(GuQ~`1c%pQ z=#K+G5+J7XSF39eD;QLSIbxj)e;}w!(2U3?jH7$siLU?iuCFEij{yG1{#%Lsq2`Wb zK=&2F-HBsTEs5B$o&mD?33hs&w%NE|8;iArV@?AJ&$Rw4Tw6!}utt)SLA@tEFuT#0 zCV{%;m27+4s*b{jdcL;?(=yHj#NFU+pm;6C-(k`)gw41=c!UjwrYi*eQwoFDiYx3V zEZp`J%xpQtD|L_;h2U?s=IYMW#}z{O>1c<(z=}yS#E7^M57wy?%8JPx4v-Mv!J3ww zkyI@PY^&N}mL$`L_3ySysNb7l?gnrf^Y{?3UtovpF}cNjpq*w zcrh+lW?R+;q>Tvp(eC(hCe;Bo>V)m4c4I6vs!B|omZR6K6l6?7OLNB6ilY{TlT?YG z(#!sw7c)G~y&mvkvbT}3Uh~m)hr6RD#}VHirV~D@Iivo{mWkX!r5DC-lahb~3n=Xt zHw>iBYE5W-dDJk9rZS*ZuF=z+wl7=1X!l!00%K7bRosTd+94-(?8a?j0{EAO6U$!G&xlOZ(%HdXI^*74l>2)7Ke|o=p?;ZPt#C<8952;WI0% zF}BC|9YugwTN!LhD`)HLZQ-ST0J;l{IQ;TcX_t1TOB!Qr?gngrVmSEn5N!Je$Xb4r8Nr7ekWY~27jmTmrpbwxH1(<8Az~EL&afNcqmz^ zaKpMj;S2HnVn@~=(Bf`P_2_>QIP~|Xr93Ay-T)&HSNlf^e?^Pge-K-8q=bz~H_*St zmhe%qXIIY5fAQxUI;&;XGj1J+&Ys&wqZ)yo>k}1>Njq0ny1+DTyEN^ghL!4n-n9I8 zGakuVMIQsVvZ(n6<=v3zm6Cs91l!@5=sT76oz%uWjRWa0H&qy)PfIb{XI_?==)-$_ z_X<0G(YAT{%yYGB;5?vX(^<&i*9+9B!EHD1_D&hzw7vy0e#TR79clS8b4u?p{p0qL z3%;L93G~imYyYPi@85~7f8Mn02%jS=Y+w{lLd~j)8FS2@Dr@_5(~`%YSW@UWZXZ56 zk9H<-jmr@dCj0`gk3m@3UVN`MQVqro%aBTNLvTQ;C|G7%_^uXjWm2u&m9WSMxUF$z zB_4TnNfU=_OoteR11Kfjkh0zkCG+vaqDb&w-1QoNqDX{_Bev;CIn8+Z{ROcO;~Lyk zwrL@cXxdbS6?|@4mOs}8CncfpJtk+lp;EbX_6^~$`sauoK}7$-pUt+DaQ?Ob~%c&d{gv>@tMI2ZGj0sHIz-9agpBf)%M1YIiMl`jjRu zOaSAz0UH^o&w-nyIIvkH>Sc=@7*&915FF9Q;FB#BE#^v zt$*<6LRj=%GH3G`d9Yiu)W-(`Eezws618~f-V{ygUT?<_*OC^dbJ8a3s1i^?+Pk-N^s zJ301Q#=_V$(u%3p>tCWsxM9$TES#`B31SWp*{hKN{j}mTUo(HxvYrHkCk%`PBeG8c z{-Wu7&>b&|(1&r8@88QEZ3u5KKH74;>^^fV-$y=c`@_S*A@@aP$%ZFp`gOs9#AtT} zQ3y}xUn+;Bx&W*0<7W$CV|qPWHsP|rYSgqetCUcsJGMP9 z-^tu(T7J4R1Jh0K-lVw!-Ep#QN!vKJYJtDANT_SP8jL^J{jXVM>G{PDQc$8=cR{(@ zTjns*Z={GQTwhbTM07QilGT;v<@DGYcl!RC@#L0YGwt+>fa~JbxduP8496Cw5mfri zYf!Q7y4R+wPw2@vv(xv-wt}{4PGyAyIQNjcGc|C`6!qe`_auiQYPkiur9EYu(S$yI z#Ro4@KVuCOH2N_j8)nRV;(@tl!VrI9pr-kgg}0Ma7SgCpAUEJieCT;ef8rM7(?v;> zL`Nzv$$^A-#by$@33C{5U@^x29Kf>*9(e$ufW>N~@UDo#nM{!>c#j*K5yW3kJwISj z@suxLzKphDK#LLYi4 zky2f-82f7AT~u;MJ48z^Hglpad$nP}63is6+T)$`?F9#j-+AhwU8U{}=fpavi6gjk`(* zFi`H#C%)rz7`*|4G| ztp~k|H!i&Wjo&b|2JH*|wOr>0?LK&YKbi4{g9|opp_)L!?><>W{i}OFw@k_*op$ z8Y8z1U@cwdlM_aEL{ZAZ*UEaAZrB%{u97MLrYlxRy?=kv>p5CO|ckzCF; zOE#?AmB6a_bE*_}Oem~^g-Xhh@98TpR-$ZYUo1z4q^d1}3$=`Es7jA1k```pcoIE3 zo<>Zk58-*Kq3l>fP?uc~i{owvOI;ah|9guea!9$SyEW>BNA5o4EFkZI9OwUn z@<)2zQy%v5dXO)fewgWt)G{UG9v`0+zWQ5RFRabhN1W{hI%QWM+fhCZJ9i|m9%_8s zewh5%!09&WcZo=Hhp^BW(LEm;_~H-_rua`i`z!BXC+LBuS8;PKvA=C|6X@ouK6dGj z@5rrXin&d}sV|fe1qo40w@cH+&iZiK=MLB+0nm7$_lw_t!EK5r(1$uo4p_iGi@1y@ ziWy_mlulM7!_Iba$o=P5hcB#JLv@oGq6mZ(#Ib2PRj-NZNmM6muH1@mxPqS(2&|Mp zz`uGk&2_jg(M3YffBHGJ?vWU@DV@;&_)#XEb*`KQh&Fv3)@K%kml>tg`_nJZ8x>wP zvR0Kg^QMF7d(I1?d!=D!(WoG&Qp1~ns-yw9e>YAXI!WDF@5SIX{0Ii0zWG5ynQRWj zZHRmP-pqqs@EK-Iw-2Y(Nhhf!*z__kSX#E9%v{ns4i?!YkS3K+$7_$5FcYE7Afwvu z?lq59OJLs<9~wktq<|%`cuF)a2j<)<5K*K7gEXQRp{mJ;n2_9)d?2_`CvQzJRey0R&mB}XkqUOF6L}pF%$8$mry%gg=kasR;sJ-bpP(7Z? zG%QNZw3-R}B0opV9zb`aKCSVKT6)(fujwBfCoFJUtIaVWshiwPhNWs~pY)C+Y?gS@ z?%Y7lJ*EFjf_z)iszDppqJgPobBg51P?Q^9i63acY`PjhTeqTIdM8YOOk<{w0OCmj zJLtKplAm(d?9@W2>ef8svvU@z!=}ILC?6RaRC%2BFSqG9of)lmc%36d+jZiv zjQ^ylqz*fDs&Ceyv+p=G)052wexSFVuzczECs{o0h;b|BkXnETc;k4JaR&sQzDEb_ zx1pG>QH5NDfJx1nFt91vMyhffdv%?0z-zA~F5$(51qP8SX~fXrg&2_G`Upevo!_w8 z?zp2C-K&aF=1MziGv0*h#j)b>Rh$ymFf-~F)C6-SwjB~al9wm%Iu%Ps_QW85A1xFy z%)FeTp~M4Uy{<;k?sayEqeD^gsi9gZ(pi>lX1y-b%*0gsW6 zRJICf*%@5ZVBr)=xCXW76L6ve^6Xo)iFLwTt^M#bC+K-V;8 zND7{FSXHR65}RBxke*8>0oc5&W9;q+8FZ*2XSmCEs@{hP{4GP3_YN=D z{jtVx$UZg4gTbG6P`=Q%sMAUE6lTyAA$_o53ru(WZZ!HBG`uFc@AA&rar|*mcwdS= zuCA)7CWz||N|y@(g6Uf{kTTaILuZi02wFh$o)6;>mi4cpPOO)Aufk+j>RB`8vwOKJ zDB`EwVwOx0Oe+bCAFWks`UD7PcCp18R>E-`-p8eu9R^WJm+cK+Vkul$khr9fT;!0dN$piumgF|AaNh2)5bYG2CPpFcTDI@W-aXa0qH=kly7-`S z_zK{>*QL=!>+(kK`iN#KfDEU2sNz;zaQBJc@QyKmYHx`8qDbe69-}`51Fc3X@lyoH zM^zwn)h^nPKdjq_rZj^*6@b!6KJeZWB}6&0bO;Mt3SZ3S^&fdphaRmV}ZGx z18V&MmmgTC!$^~2w_FqP)V#=C14-#E$q_1{;3g}YBg_x(SNU+rH16ndBSZzM6jO>D z;3hx;wS?;^C5<{x4Ifs1B4*HA(&>3B*T)yN&o9nMmZTe6{zzro^!Off_o}8Pttgia;*u z!AM{Ubh8E4IRdX-^b zGJa^PaAv)#eaX}K(yjGnUB}C-ZZ^)YW>-y%JvG5MRwn^_lRym#q0D%1i;(HhNT3xw?`&U!obc3o;Nb2B ztb`~EA_;j-k!hJp;p+wJ zv~<_qqP+d0?}nInL*X~+5b{NkY1ENFq@rFaF!An+y*m`EKNY*ae8YQlr-oss5`YhX z%#2oQB-Qm$l|;akMBtXjazn;4N5x7+LqAj0SXZ92Cy=9v1L(Gt0INup{g$`^N%Bca zy30v6C`sO^N%3h(US0i8cpywQ?d%pH)U+Waav*GQAxd>;JwX6yz!_t&(qRwEx<_%r zJ;_2}%F)w`>`BOmONiX?%F(`*-F)Kue>i&&Z@Bw+ZF@%V3^UA(5~CBn1tHOUXLMq; zAbKZ~=)L!nh|zl|B$61tw;%{&v>-$Y(n#LPUG~23{r=YTJZt|2*7~kB*5|se<2+7q zyyjW6`dqq((lw2YYwCVQtA3>#;?@Vyksfw1ZSG1R+|&pD#DJwDIT@tEcIjtLGj5{v{h1B@frb!vZS1R*Y(~Ru zX`^;IqjLpg2^E7$Cnoo0zRjmHklH45o37MbnHEOU&Em>F9TAyQWB(`n*_iWk^ZO%{bNSxbT&+0-hGp8@SVMrs}H?sFLD~Pzy|`VIdoie zxZrjO5Ok9iv(LFh zi4npKZ=p;IF?94DW)8wGb_~`5Hyi^3 zT>_rD->kX{xM3a`7##TQfgL>)A2vS7b7NCicC`iUn4;^@w#IbN)AR;9++!=lhb?WyJcd@B&DVJ5Xj|z8%JwigGjN2at zf?qkDN?m$froSoAzcoa@h_`IVw+Zz9GlRJ8#=p zNyRmJPaj%ZCVCh7S&{9{HLk?!4EV180TAc9uLiC$ByR_<iUWYoi0r_AWyWD zv)e#F7nqvRgv~vdQ5|sm=&KtAz+XjNW8+aAsex8^NIdtmX(KX=xe?C^Q%9}zSQk+Kc6-p{`hrmg4$}X%u!bgX?qc- z*^X$8cDR{c{KaGxYf#%B(zrl26QQL@#=m%-V7(0%N#pKVFRI?IVfZ#g`8=IGPg`$L zL(XQpfnLaQ?B6aX>6o#lP>kO$iMgvOLWcyo3F_y&8J|Zsis>cncm%m+1ym5^1Bcr zhRaqT+GSpC15&|(tPBA<9wzPKN>0abONH_!9(ZBB{N>%5|0MfV(C-rCS9(=Wyr5oa zmZTjOkR5bcz0^DA^;w=C%S+%t)5scfig;POk1@MEB(~cpw=36pcm`mhXE?#+vfOVM zQ<-r}hM|Ry#-3vIqlA2;9zHAFkK|j}=!8W@y9WbgoQmUEJ`FQ0LAXzNyUG}MjDD_CZeEM@Xh1HvNUJV(K{mErQ;-MJI2|XwJ=Bd# z=B3FHV3pCa2FI?ww|+Z-$gi{(Y4qI z{h(UM58;{30Um=4ADJxqBQ#s|TShr*1)QKd1+6)C?u&~f_G2pPTqfF0OU9{=gZ24B z3r+6H-U-|a(t{)sBuGk>{hW^Z!`85!xMNSAAEVCV)KvDI0n>f_8Xw&I2f2$g$&GwOj7)y(^RW&V~z(DU3IU&EN1BTco`X*ssAM$mK8e6TbFQf-K))DGUd z2&u=o4PLu4uBqv*&4C}G_t&TB?Tl~7p3n#rty(bQGpfd(BuOzet%IM7>nEXJln?`^ zz$G^{_pSN3Rhq_UAg(N#Y$JDQWi8|Ch6npAr*1~~G`&?UwGmH#&^YgXG_`>JHE);; zT$k3dnYt<$8yI3Zemf?j6|3IWnK-Er2Qn5o0mMB-ZMG1(fa63@z(Tf>UJx?PNAAA=blzI(O^wLif zsqOwjj5@GRZ3(cMSo)!){x+i2nw%0N%gjz_IYsnbRw|OgMpxv>`sh{Q+SU6@JO}-- zCQkfgE4m08e$sGa=Iw08G}+knQa-EuNz+}Elfr_P_~QPwGoPdgI~6_yWo=T-z*?8T z&}R>G2&tPpnJCu2dIFU00LYyjX4FHItz_3A^T;V{A zwRHdu&<>X73=5~&l0SnqxzZy5YwI2~AW9-sF+G|>3PVZLOhtzUQ1rwQt?LpTUx33c z=nM#`!YyE2&Tv!$IIQ3^Ach7cBQ--xi)x@pP||{M1sDksApMTl8Y$Y6Ds2YZoD4ll zhlymvyz^nrsYD3%fX%f)fvvSCe5|ZI;0U->boEgk)v>cq_D=-2X0FVs^+-47_y%82cizwi1xygR2#DV(BJgW(y z)#DWT@W*KE%`NKBefQJY-Jj(LuUHyeOgJ6N$Hyy$dQ7MK&GhE3sNeF=qIp}&u96NS z500<42}1A=?#5+(3UA`BCz?*CB+wxz;Ok^WC%v)#NsG_~3#e0LMd3~4fMeP>ggs0& zFyJ(6Ev5&SCPR%Sfzq(mDr7)tXsEHcWJu49f+#Iop8?@6Frf)TYckeM5O`_wZ%o^= zEfzI5z{rW=F+mU`_0sEJ&qxv_#E=p$rzU2RcpekiW1q0p44c{p^_NkoF>TPtT95DCR=O6NZ zr1EW)@@F*gH@YonAO&209%ZD+71VckGF}Ppq;VYQ{hSH(Y5+9#_h_unwM{~}UQA+Z zM`oMMe6pc?8l>lasOr6c>Igq=RN(-Yb7FonE2|+Cis7i+9GHS{cxKm7bXYnzAR(J)v=)A{mf!OmS+7;aXLQ8pb^Mle+7`b% zWR(w?g+<@t318)zdOm*i$~3jtjCzRow31H(sP9iODfEtSS2=i)Q4wKiIp*aQBmb1A zBaZVYswyI&f{jtZ3S|2eHwMCp#u?B=Z^Fai6-ajI>N1o)grfOp&{1$;+nv1&(uL*~@m7eyc|Z;@DANn$%mPP+hJyAA=H@K8Fy^BZB>P6*suv0XjMF35;0huLh0cpc zSy=TWPT&fQ$$(qVIA=~h2e6QJN)~?c{0r8Dsbh@`c;wO7Lb&kRGXU)sNb2>B((4P& zL+uX!)COc~BM)l)hpVfOrYA4>8?W&dZ#Jp^4Qby%IsK}ed^H)I6rZni7sau2#Z|~7 zM^5{rQcxuakG}jl8oM!ZmCT~`p}5%`(EtlqLCXIGVQm;=e=>&0BNeEih?Ym#7I3>N zSRoGn#p4k+1paq#%BcY3E2eWl=W zDS;Dis)t^O4R4(kWN8C0#l8Z=qzJ=;@a}Aa@5Tgy7a>kbmvY-LQ9+~;Qk-E>3gJZ> zhea@W5o(#oB(R8x2vP_O;)6p4>CMCSdc0J2d{CYkRwDicP13$D%DeSRarOGh#nW;Qb+#N7S<28;~<0`vv#(7lnfORg9DD-%?`E9PS5G5 zZ{*BzF8pA%Z=pL2(32)BGzY2W(xrbxWJq&n1oP<4h9U`(x;|0ro^2IbNt|E!CKv=O zb0z#3Zu@rWWDCvff6=&3h!;W<8Of!b=yH1JT=32_JL8K8{`^1F$U#uF&V2zm2uPgw zEd>Ninn4pWAnJI~-Owf)5HN<(ZkQ3J6M{%!ln&1g?|=b~^3bt``OjfM%e-(&I9p<# zc@kJ5F}JJvL3j;g_h%8x_E8Tus3Vd0r%3c~?Uj{cQmqC_?n3q4dF8_|I zkTENI?o#pGz2dufrHp?i+_CccgOUPB2}-2Op39V9l-|3lNMl#6v-$4!_kPR}6Lg>^ zq`KyLy>ifqOWu?!1F332m66mJ4~G-awjvVol8d6F~sP*i1DnK4!q9ae}<>cS$lEcPm#7!qTLcY5pcyj zh#fXy%$IzHtju-VF^6?f`TIUKb2k1izLU*f>E!Bf@$xqnKi+EoLo(&9d+Xf;{~x#{ zw{`Q1d&dvCg&)a73tp0sxl%S0hyP8*e+YcJq7`J!BB7V<_`y2Fw~px@yM3<9w#7He zZ8}!$kP%ZZ^@lH)zj|mW{vy#ZG}`}^Vps1)36Mu`gvCG;?1XK&sWkpDx99q&8D zkVQn`NI#e^{mChg6#!7ruk_RQNfnu1H!F;HDKku(nIZ7!oF#~6Q=e7N=DPNQ_daVF z9=7a3d3t9TGcLCok~2En-!6FV^b8}S+WLP=a{sZ2nHaDJs~V(s9jeU|*K@cV#>(*r z8WF#gpe4^b1S8AY47K^n>`vuH*`3dgkfrVaqT-Fu1=RE>no2x87b(8^!l2YI!#t~o z@Y1|R#{C8EKK>w4?redRVrwzR@T%8jR=jMyn?I1wSCh#V|H=WB$eZA~wDZlR1stQ$ z<+(ig?XjWt_40EMdR2uHS6E;$Y0z@6&!T-HH<{CrIUlGswjeLj>mtvrcGjA^JXj~W zt!z_QJ>UFyk~?2eCvzL7i_G56_pr|0fu%U+@8mbQ7k+^K7oz}E;6-OH^Wr2%GCI+A z(C_!w#$KtemHiN<9xc3c@1GqT>e0%MANlg?Cdr=n1QbBAGrF>M*TVn4$vlW_qYE|ZESooM_bLQ-c zkj|TPW6&y>U;MvIv8hRU+zI**K;mpYR!TFEs0YOrB$PDS zSlHWv}=%cSB29XSp}|?X(SoMFt*^ z$d;6s#ACU|hW;hRE;Rbj5zB@E-KAAr#B$O8z&|;`NmSrN|80g70()htL_9l%o3{?l zt_DK>TMIYGOmC@d?h|yJDZlEsMKvvt68ML z5o~BtpdoXQ(*-BRrnO3|trov1+qAl^LiqnFH* z;>&>FSNe_bgPJ0V!V_((VcTTk{m;pj>Tq=yCLWXK?_?u+qAcAFLeiEkbzBZrs@qK( z7A)Rb-+S|f-Q4fViedVM=G+5;pt)X^H@u8Gp9p0-#Xn(JETdpseM_AFg!NCWWy`Od zy{s>pcC$9v{*R18gRH0WY+7pl-MW^j(3t2G*p2b?z8P;jLe6g>hEGlJ-(5XACIY_R zO!J@&ul846UuTLe`9&EX%1iLI3!X>*JIVS76>s|0%i0PQalD4;H`DjC#fn9#ak2l` zA|^&;&}<5BcawmFpS7^5H>cmM7AjQdpC;L%QEjeT>sa?wW5isD%VFgS-66S4Vtvcv zAB&hl^D%QzLC2Ms>f?T4EFPO8mM)}-v={x)Tocx6?mhDU#O2kwG$eCZl)=a4YD_gb z!Y0dpTV!DCW}fHI&(7AA=rh&$?%d*_wQ`aB;Py{EH+sL_QM&n{K&lEP<91_j<(^?8 zO$=EByCDFOAruQZV8C246Nmd&p2s|8isZi?^+Yoze5Yyw-e|IasVXJ04xrxjrJz7? zSDkkvsqsSHY$xnnFJfkvE%jXOnLJax*W{i;f3ar%V3!x=RcI+qx^lRh==!3%w`z&Q z{o$N0q8BG?mj9)NQ_V{HvBT}{ZWbD8#OEnK9IfC9;e3UJy_5D@`PkR#C^bb;Qhc|g z|6z{wshrm*{i3DY0H>UUEvwWF=O?hz*nl*q@9G{;*}XyzU>04jy2H2&Hr z{iQtilp+{uV%9+r$*wY$#&eOVAT2e8-Md+sB7y$e2h@tk9(uq<(HFx2QhUbw^V^iz zOO*%97xaBEFCW-DvM9o(Y23G*v%A=w4nEs>P6of1qkHCGI$HQ~Hsp@Z-j!PQxlR4- zQ6J)3)+0b*&7JUWHdI0NLpQASi|7;T2Yo+@L0^^c$B(??U({Q8z(m9HHJRd(s7k@p zqN=|A2L?*#KO2{R+_BQXYV1zB_2K>Ho!FBHMOQ8{YoONM@+{7w5o{|!EiwXwsN_TD!a``# z9&ndhyzuLIA83e+=jaa$3Q)r^1w67I6K_BrRB#nfp#rpyA@m5Lp!&LpL#R-S6a;=` z(;CF2EsQ`9(6O^;aStszr~ow$lb<6l?)m;{K}{ET0?h)m&XZu13KSOMD04!6VbHO9 zf$keJ!R15jHN15b(6P;aN&|qV2==Ly5hzKi5BKbu$JZlc0ge!Rv3x(q;g%+RJ#!1k zO(!ChG$%Aq^yr85;LQRZr{pZ}#$#5f`Bt=3K9dW6K9QjFmZoc#m8Vu5b>(@_wSJzi z13oc;h#(=Jy62eQvMD=5N4|@Avh~=Q^%3wFSEnyWF<(W38BRjKHt?x5-}L)MSgW*%de-IiL-!6 zqZP4fi+;Bdpm^ntbh9=a5m>V7X8PjUL&@57c8(O7*n<8=wkAt`zHgE3QNG+xTE^Cz zaxKoUq|}Nqj`9=&rBo;?Y)z?^&GH&AJKoAQs5GttSCRZLIpqI-$t4H~@S~QUa!uw9p`c zIA7f-W#=luqdPa4DG9y;0J2sqb)U$00X6ydZQ$rjk*h9eAEAbIf-%9QF>P%9woycN z)MBd)%qf}X_?6cp!{L(E%PHYkhKR;xwx5Jc84IYyok)eAnXEn*2IWdN?k0uzm)uE%9ml)$-+RA5EOaCH#?QYkR^hD;r zY)${^*gB(P&97=i3=t1`6WcBuJV!L4>}wqBuXB^-w5JO>JMgdTs>%82O>0xgDz1$e zvK@VAF@O-4hLs3i;xNY*-&|8Qh_a?vF;Lc_RW=}yqf*lE=MS>_tN5nJ2g;#D0m+Wp zp76Cs?3&2u-GB`%6WS8)cEPsZQLyB3s|@>qu)6@xj0%Gy{Uizz8Xv1zg~&(%!P3)k zD`HfqBb@^kypK{vx*~`Z6RtDL&3Dn21^{iY`R2XV2R7~$NCobQ}3l#zuojopCIYYfFcm*XNPUcpyS ze;-??&aZwwJ-Yn0{->>ptK6c3z)sL;n*Tm8X1^+Cw2WyqFHpYSxE9>fXu>yPQD-LG zjKIT02|vWO`Du{Oe;?vVB3@ns#AwsxgU<|n{3%*=xG>UIdEo>b+_O(w0H3K3fEuk%i z3s92`4)p=1nA4-~CZUR~a;36uCvg7G!4PU?`8W3DVP5KHJL7}vgA9>Sm%#;bA$ZMq zhU6+YvOZ-t!WZoEKJ;R3FVvgLG+*P- zB{=|}_-PI<4lt;YOj(^+Kj;^!gj?*kh@O!BSMiN5~oQJ+24D)Ifo=H2T3qv(@Y z>MXA4jP5PXH<^5s#?{h3k9RbU3VG(A;n`YIh99evVl2TxqM=9tK7U!U8xDyya04liBu>Aoy3)4#uMX5&hEtH8TO!!`I&NEFn zi1CPCZk8zSG3tkJIAzanPic?z(@Kjsq$@nXtLl8EQXxSQ%~arV;j+qcQ@kQ)hP%}G zBi~h(giY&j_4B>e(8!~@w@S};-0yUCJ#sq#^%zRzZ7civ?>D~m)*@Aeyb={~_a-TV zpVRA=ss5tJgQ>Y$G!JPEjpCUX)#mW`9)J@)d$$W|0?UnSF#c7km~@WjEe`L6D=qw$ zdq>2J0z-n|xR~f7@jiiipE3OAkQ2OZ1O~ni>W}ff5H#MF&SNdI8{B$cQ?n>hz_;K! zY1Me+bbNCxI96yfyMq41gW8FihWofo4P&xnBUKFFa%@SLM?VlD{#{LD&swO$Z|%vO z@398^)_OI9%#jav2K$w7!e|eoj9St?##L|LeEi3;Rm}zC(nJ;ErA`}>rUzl3@;b@- z(nZJdGf?+*G6qGC~;7EO7tkyhpYeeO~0K>uD z-Q(A4dslZFFWAo(@#ULv)U_W)g3#gW1GsgcWx=DW_q6NES?ft=1LGcp+RpGJNlwhx z5T78di8eIc?o>+sM4>hq3BW~lR3E)$){`!+cZqgw**4Nk5Wa5Du4Kp$7 z+n%E)&+7(j;)KTsY_|!WxAi=U8~ur=0!e5>zSPX=D4UZUr;`e2lX~ToHa#IdMw1Db z|D}FLbdhryM-b@4y}Dt?Juv-#Xxi|uf(Jd#^e2=7KKd5z^a{vpT@cksg$kxZ8&P59 zs5yk`(!NobiBNleqX9}&Z(yir$f=w0Y0@d^ycuW=2tm!%G~Vnq=`fJUbr6Ub3dlf-5ui+) z$2AfZA|6nL$5W_6C}_Ij4*C%UNOD9M*~uPD^C8R0$u*JlYkzXcr$AOw90Ezo+Cs^? zm7!zHf^RDb>=DMZU87=M|3zcSIpp;#nx}8y9YB!7R2N1Cv|~)*0ylCD*M(ej*k?l1 zhF}Pwx^$d)79rYUyP) z=jNMY?Kge9ZnnO>`4@*gBHKD4`*}vz&O#-FR!5$PfK>)hUKXF#gIM01EhFwo?f5-P z__VBz1=~r_FCW-bME_xz3%CWVKX_&fAP+BKMimt6d_6hPglbv|<9Gu^vZ5ZC3plUX zrwm4SZuw;X%^@4>*R>ca4Df~@jBTrqBUbqwvbJg(iA5V~eO{}zyi_}VK_c2aIi19t zF`WlYj+qF^8Q;N8~bY*x9b?68z3c+NPlx=yEXD0 zg)B3dv&)w&>&2HhBnaatD3%9?apBROsJMpcVCK!rw#|@#bI6q^gkBcmEstwBwQKx5 ziB9V#L|fJI{`Y9>4JT!dVm!C)|0J!vOQmU_@)%pCU0DCT)S_;tPV}R;#m4vUXHD1Z zG%eXY#?#u2a~3*u+FyG5u2|>Zh+a4h@L$vC0oy*-Rp!srP;_ zS4L=`QA0?P-TnO2x4%=L7IIvuNJXiJ5SLJpi5}v->|8Uuj|blI+>x{Qj?ichtiXu%if5Mw=$Vk zb49SZ0k}nVD3ylpo`WCG#I7>kpCvBmc*`tayYFE{R4(ph6a$g9GA49a)}Kq&2si5F znrrf9M&1+Md+t&7^%e+_rpcDZ5w2{(FsP8_T(Bcjfb$jwH#osnNAB5VaD1~OCt?fx zs*oXAkNS@_qwhSbulq09V`BrK<|gUrz@wNC4Z)J_Gk<6MDI3P;E0Lzx86~8{&fY^n zmii3flW1L7Zn?0>t+wjfw}xuvtoLVzR-q4P)XqVf+n@n_#ck<7E-Fa|*_ikPrW{4m z6*Edgv}6!n1Yli=`Wh_fKV|z3gh$Hk_k%xXRCESoAy#dH_PApd=eRHbl=#$5x07d+ zFJe}4-RVV#(o-%R-?R1h3S0{-Nj>i2YDW2ze!OZdC|iAbbZ_CT`_sGO6IrU0`(rN~ z8_3-^4b2_MqwUl^cqpSzOsp7GVFi{9pJpN)>8~E-IOLk;n_1@BlkBSy0HyU$*F-dp=xp;x&7Er?aRWnK`j{pIg#BN+W*m}a&bBj{J znVCqwjw{->YzE%;|En3D%gUs6`FwBBfUn%IC8v;&P7CWT+>xQXJVGAmQ9sJ2ER{@C zqO209vz3wIcbPDU|42WDAV+?<9x|#Fhs$E-OZEOUi)jH(e=FlOJjK`41nX;JBSyPP z#fb>Essy1Sy}JepCqX=fwq0p>TLuZ(AYPTYZaTa?^L%ej@(6V&3RL%PzE*u1V!#R` znXMOf$gsW_;D2ph@KvSWMx7=+;JX0cZw5wcVthoW_jIo_~yJG${-0q=Tal>uH zrmIo(DIfJgg-ZPD%~5`;urzp(o;j|JrthjqdFB^}hi43=&q)GU1mQQ?Q7f9MMA1C| zchL_2ctO7iAE|+$l0l2VC6lxwPA!~KF{i=kpjmFflHFfmuC?c<#ld^ZePg)zeHL9< zShGp!n?*YMSjL3JaucCshHKF1&lyQ2*`qm;vzbG5_Trjlu&A(Ju#(gxO5lN7JW%#} zR8=WGqF5?6O`A-?mXi?BBo=dg%Be!rLfe~W9uxMJ1FzVXMuFlw(SV`z5bG!iXs)nm zNkKbEvL(35V?x46i3;BhfwD(9<{C^3yCEb=?O45F*hNV~+v}~0vbMRXB=&=000{4N zGJj_AI(~Ku0BF%)U@$GvALQC04}dIOGoCD9O3BXEpQAn8<%d zj?HJ^^s|bs8*^&&1=|3_J(zvNjAVE)Z6*6woE?SWMj%RlUN z>dRNf*=6-(YI^~AW+9N={dLX3A!BOo_p+SnUvuy$ugKn9@J+3Iye@hCi}wN+?t8$d zPazR|LWJm7{y?QS>kv7T0aCH8;=rHQrNIm%h}K_4&xE?;JVVk$W=AacIHYxE00xSk z5Hy&psBs~XGNdeEkxbB`-cwd6@6ye4)U?lT8AMW?h`h{r?s3DIEV6GgRg~2=TtDH7 zGJH}-ILVY9M(CTBw4rF8&al?up+5f4ntqZeVAUf8>RelF-AWKs$}cKEHW%b)PpmFZMkZk z;3_4gC8;;69BYBY;snk}eN>OL#y4f52%Q+Z9}qWkEM<#ElLk%i*S#bJV;gDH9dIGc zj0))60RU()EEwQ^B0juS(0ob&07BwE{OwuT!Ib#{HlemeEKh#@JyQO0O>2#=YxrBF zP?a;eAZ1lM+uOP2Ql#sIb)7^`+rlzd;p5BuwH-vgT<_@YsdUm?I_%yqy|Z_5SNzca zjA;2?te&R_<-m)Doo4z-5?7CtPT{D6NQjbs_7UVeMz6>pV-$6|{^5hPSJH=G-}1}J zZlRmLLIw)Z96suw<+yc@xzkI7pHl~hlj&vxxXNOBg5oeTJgC%Q7m%i@0oR=#>;1`k z>Up5eLEPO{U7nUZ?PACgZ3?khq-H9`XvoHy9vLQp zIGl#7`4@nW6k{w%@aq+AZ}pkH5Sc<%=w z2HHqS-dYC$j+Mx>8$`d{`ABFP>vCw_h>B3}+QFCC`_yE7nR>cI*vk|iMbqoHIvKh~ zBz}Q#0hX@GY<{xWvszQ(6JxKrb$G@^M03-#!mzyQI|lxPy6s15_a$M+rQY!6)b!@m z+*Rs|3H3TxPtZ>f&7V{A08ZOqsaF8%vpcA1sNOIL9SzW@wZ<2%p^6e9Uc1N1H<}kX<-AXCCO?`u$E$d_a9=ZNY^5Y&>Dl;%8h>%TT-RpPnFJ-LayJm((U0JJGbK2&2fBj32eTp(9#hYl7J;UZzHvtyJ$_bFKJsopLa4xx%y zDK*#w7a#@mbt;YaA@FS7YGf_)BVD$&JIvbKbghG-J^6#9+tSKHu7CZJdp3h0q}^<~ zL6bS3tc*Q4D9K+GCp`}(BFB&HLwQ5BO|Id}>FrOJm^}0lStE|Vu+Rld>HNu^(`Rou zcV7nbt&$oI*D=Kb-pX&%T;)u;w7+xz!uVw2C}``w)K@ANF!I^JJ?~}NoI3)E6L)+U zt{{OrTJt*Rv&==70#mDjU)ES~f8gD=UGRijmZE`bs{uchtbL5xqRWAPyb_=<%lm_j zT#085nM)+CYYB};IM`+kAe<$r32si>$yVMQJkD3H%6SVSCJ>;Oy{nK+2_R62@KHK~ z4Qs|015D&|W5cIy2O`Y*G?R6_=3KhD5kc}{z&#cd-0s54wp*2yhBLMd7u+DU1@!0- z;WYBs`lHj$uB#Jn>4CLQtCN(Ke9kap+Mku*NN63GaqMLe5p0A%F)CRIR2oV;J<)xG zo@;wN3F<}adu8!bD}k7#C6vXrb;MK*9ITbl>Pw!gPWwzec7*2eO_xO-I>5_2R#g&+HPpV@1 zx_l=wW2n!KoPkz%%Yf|{?yO}>W{YeApZ&aY3BK`|=MT4!1zLZJ-VC-0EZ<+UVt^dP zvQ_o+C03$>iV-nvc}Zz;F4D(!SXU@E7WFm6?J+7o_#&89&0y<6`B3`mttV=)c+(w% zxwJAJzNs2zJO0u8`z_*e*Yz?-yyl$`ExjRiJLA7@I(-b;sBfnfQ=buAR#GSz`|gxk zu@@3hTICw@FmFaYSxkSPO~Bp?V0HJ0vqv7=rWG?)wEuxSjgHr@dBNL-%`S_Da=ZB-2leVNI=7Dq4ND}PDZia=VK)P50di@fOY(7W_Ihzgs0E{v0H0l|{7HJyrv zj~PRScaTHDHYUk6JGEKcJWna^RQ`m^pOPGklj6`39Z$+3{Dr$h{)jlfCp9O0(0_!!IQIq6*T{vS!Jct=mJ3D_K6WvWWqbs%ned z^oK#nWK8XK1;54|(FhAI6oz|?sV^^+-d;O6UDU~VEI(7cPE54hbm@i6UG0;n^c`WE zT2blFj`JnC9VzqhOypzV9BiiQ57ifD-`&d#1)WI@Cj_hSw zIq7v+B1DvW9@wt?4h5z|geVQ`x0!V}8MFL?*ldC^vg$5n^MYth=ML0f-jf`YB}vd! z4C%$WIQ^e!RP(Ge^g`d_p^YW^vMtN(!&D=C0A!J0yK1wNd?;8m?nTPi!10@W$oG4w ziD-72Bf6#VYWi#d03_5n$v)0D*ev>-qo#vE$#kZZui%)k5y+vM6rw6z8`s4I$r+;p zMkB)jF)bYyQ-8IU#SfI5d(e-Lq}>yXSTFYe2L`p-h7-ty zc4yaf+d5=M%RI{u-J3wRhat$NguV|;UfNqHpmX}a0p)O`H z0}@Yxm&q0p+>m`*97u_|1uHm?XP$`0y>G^ORnwtx#fPdS`8>+odA(32P2=?N6n>f_7Pc+|97gg%X^F+nQLp80 zYb?1mk9(<3jk2HOh`3d?i3(?bsmti94op*h`=d);YcXhAhtJZ*%Mbw0h3xorffVX; zL1Qx3f42yeXHsnTOL-e{ee`5n7#8=@?k;8Ee>E=fS@U2cSGI7PLtNXPJyHAP+)W7_ zE?TR-VRE1wTZVU4@a~>8i~BR9z24(Toojj@uFqV)54>F{82Ccr`GJk7B{6%~7rXkm zAN?C*!h6g3qy#NeC@s@_7A<(eNzd4us~TmE)5cU|nuq7KbN5waEK7}j7%=Mq76u`g zX%whtc-^$!2}Hp{sPrv#K6g5lo%Bg8f)U84V$k%fM;PXFdeh*#v7ZVN+UIsf-?u6s z73wn`*x;Y1NqxnL1y}RXG@`JnrlK5r6^1|}{bpTbg&teh0Z#e}C8{+MCsH*KfRfY_a1-bJ8 z02fy95lovVnZgWBOJxp8XMIB;-$&nCt@%`|-?`o3wcPr{`wRZdE}aL1WK`N@G5ca}f<9*&F3|I1|4;w@V*sO0lUV%-b!F z=72nZ@|crnw3?zk|FJt`#Z1{tlEd5Qv8JG<%V*PK|HrDNMJ9ifR)2#4m;eehhYv2%IkOskD z*Jp^k({MPFdE-+i4C9yp0uQQTS{Wh|IRN0yhDx0Vmz1jnZ^GGn!jss5yK*3W zqU$PQCaiW_!Y~76a|-8V;D)j$aVZhE;#V7Kf>6ykfPW9?+ z#TpUKcGK6q^%6V%OH8$UHkrA0ccOh?@F*I5<^Rxo7=5tKTQY7+0UMQw?uiTN-SA04 zHtE}py?sjV?UNYWNfn)M!;R004Z}+_tZd5S!7yo3z}o+85!QY^G0I~)lJSd6n=t)v zbMfI?hNRPNjU{{p6kOldqF=NSy}OQc7@{~?0kG(}Be)_ej}mjCwdczNEA0wTG4LRd zZwjX0%w|jVZqx|_2v+-Dg&XgqjbjCj%i4eEc3+x)Bt+FAQ*bT96XqXPTs)|xU&alX ztgE$qd+AcyQ3X=ne5@s}-XJU}^?`NkacPd<0_E(YsS2u&Gq%};Yfy$*UxV3z&p&Fs zATzl8_Sn}N%7#T{S?@Lx6~j@kIf<`SphN{ltcEbYpA`f|@Z99Z;?t;&3H}x@@w(-H zdV4d6`0r%6*Nh_tx=pm5-0x`j`Q^hHj)6?>V+Za0Y|$NuiTpRWoOQtX8b)Ndc}~g$ z=Zc+i3TKsP&5^-a2mbC&0_A5-<8UR5tQVUaYU3nS!7YMANZSit&gpVJ?I zpOOuK9@*e4GYW^L^x{ldJ!5a9q5^0rb4QQfTOK|Xn!m&i#J|>=2p4*% zh~(`=IOLDyIB~gTi^iqkpO#^Du^P;>XzviO%;12LDU$IIrIpoDk?-1rsR>Um zQKr&tpGj-ntyxs&a>34z>aSC>idr><%eg|KC|}jp3Fh+Hp&~q`K(nb|#|E+VL$P_X z=JIvsX@q|N|3}(e2F2N~Tepq7(`e)FuEE`*ad&qQ5C}Bx?iSoVkU$^_jk^;hkkB~6 zH6cLq^($-bcdc{kRGt0(;(r%aPu=?1(KBuA*d~)!0sP z*voQj^l?7?aw4a9u1Dv_84$I?B5S4Tytm@oTb>E1Fxdz{$P6xB6%MNsN3zsa(-~7l zjrBCO)6`c;(R(E?+^W!CA}|uxc$4ZvTxn%gWpl?FV5dn0+piJ|3DuDN4JG=Gw;hL< zcJ%UpqU2wrYqlXj-;y=J%!`qt#QX?t+@n*hI4;@SkTankX`5Z)FIRri%-(xs%6$vm z%l!OFhhF?Km8Euo8!hB4GK3te_Ler)f)|%vW}OtF>LtRvw`r`MBW1WT`7Eu-Jy@RO z109^MAQo@z%X0H_L?1-DCdwA-YZS+AEmpo3PKwc+ce07$jSavtKraIAE`#N6swWk1 zsL-Cq2wnDjw-t0n4WWxrD(>39qvSIm4BiNS{1|k2ZG4ap13wHCzXz-BJ=U%>c+Yf~ z(^2=Vb7ZD_6t{0wtA8|gXmq^hq$vtT=kS#KQ_&9KZKE%{%%Mp|&{x|EoVG{qa%ARN zz?Y-D=O-V~VW83^DLkj{QqM#3NnbQsWZ?KWjvYE8-eY)O^lDbHf=BdvddT+VM-}$3 zDz8otZJk>@eb0S0r!-Srgb5exEfTtJrt&J{W@9%mXUaqm64(;8>;_9lEQ^vkbALC5 zTHQRoJIX8;IcgrcPa%0tIR(NCpidX2PY-4Ld&)~Fh^OHp%JKnr;sxD*RK{FAAbvc} z_Bmf#Yh7JC-=S_jqMkjbA-JX9n)$3w&*XJSS!>=gQGkaSxP_pOH}LpXsf-OFkpq@_ z#tI>Xdb2^ySu0)>MD|h2;=f29VSyxavbY211X*k{xyR|dE-;985KxkePzJrX&je82 z>PQZLy@LtsrbkD_V-IU^upF8Wcf|Z1B_D;eVso$&7O-`cu$|}+@8+QEOpH{E;4n$! zI4I*(YvWviahWV|eN8xB*>F!scsnEQ1x<%W1rh8cXtr?Aq=%BAtV z$p{dTUnKVcAn(Yb`tx$H=`+Xq1c)mxs zln`5|cvlSg@)6IG8r+me3qYsirPkA_KILDuwWN}bCZMk(F0UY4v?1|x5`OJTndZcQ z9V6hweq_3@X<866OQGQKBl!<9apf$V5A;#BFW3x-8pH8jGrIP(W zQ8MM_nBa7zH~T&%-zV?uTB^lHYBUCFWe)1BUI~GZ8lCeR=X*4Z6CJ+D8#u$tg1d%` zS1&KDPz8SNXo;aNc36}SWPM)!z#{#S%4`U~fAmdu?O#YMBcTAJ zfMBDvZiDtYZ@zhPG;AM&^;VxRYgz3YeqEY^(k^Aw?<|a zFSK6Yg$q2%;EJHOd9iTI=xQnJFi~RAxmax7TXI{Y0T0m)PB3#W@NaLfc(Zg3o~-T= z;Ol-1n2ROty=B0$^7z=>5pPRvJQc0gmncCiBtaiJ7Zdh;hZ zp+G42sjJ!)SnCp4;?lWhdOc-(J#Xf>;s8`NC$urw;zQMOVV)7+fzip7PxU5*f?-@9 zoU1tHV2^4Y6}l@L!(9l}z_V<}^IT_eiODPQ-mC7r2g%ZO?mCd0b)jkJ25<^&l8Ep@DZiW5&;bipYxc~eNg{lA?({^vXPzs-_yOhTjD-b;>}pEeDe zjhsK31iax22(vOSI{43$;y_KygnKpjb}SoPrUu=6#0VG zK;^hz@w5ED1h)3RCK5G9TzONDekPonFrE-8E3)SY`)CMJt~NSB0n{ua{}R};c|C`# z(t6e8W{NuM-Cd6{zkK-6((VzEn_|&y;IZ_Lo8m=k;coRETm}T6J$|jAm(Y`AJPX+R z2Y}z%l2JKpPp*E_)$df~&X_b2k<6AY9vW0i_X=PA2?d(&PK7prpA0ukrZ;i@0pJ(H z8B9Nsgai1o6$$<^dxXn?NJj{gc@0LQ?cI+`=coKbMq`&k-xMGCN52?L;q74QD)>1?!~?sk#}Ibh$hkeF8!r z;P2R~_!s#(<5U(a%uH(jOMy5sHmkW@DgTfGt&;%5_40C5kqmOXTs|9r&S`#cw$imm zWw~;fGW|DyD)$+YR$#6&)MU@(ab~q8tzKsKCok|(b$=4 zS`lDh3Zqh{c@fKm0PcHUcfzy5))O=C@z;}n6sr8ydg6|SHAjQvf4*aT&naha-=7zq zJ$-~%qLB;HLwT_2d=T+~!*P$!VWV-y@d4v;pgNDq*w8V%>6nc}quDP?k}&Pn6$syA z&U;N^J3f{?i&~e_R}QrSquJ5VDJ;gVHnXWtVUESIur^mpRA{$1C{8ieA4E-)u(SCg zrPM!2iYH|^#BzFmF@VZIA9wgskwDC~L&Z@CzE~Jcfi{*m@`1LviX6hH& zon#I`1D|}g#D+zkkiQl!tN>G>8Onn$iwTf$hLO+=cDn{O$^=4l!-aKABylZJh8WAP zNhT)h87-IE1*JdVLhaCxQel z(}MFfzlTt@O#vv<(;~W^L&*t9#}~WJf!(0trFjn(bt2v=&dN2^@|hD?RQOgBbgFqY zELN)zmjV4%iDtc+p{N;RoPH(qapNy&fTrDh*}lbLO@P%IYOlK#r38@YaR05wb`L1E4P2djKjsFdH7wB@ZHx9%Gkmdp|Q6{><$~BOL@I` z0q>s2cW=7A=keU!%>Eulh=(;E+$x(-Hel#5M?e$e)kR+wd(yndCvWc_)ZJc*Ih;HQ zwnc?48|`mH(-gg|(bhW+f!BZXr`C~@CLroMC-GWrA(*XeKClEju%fq?LtT z@f?hi@hBHSmp}N%B^Teie^$Fw)M?}C)eR~ntmn{Lp#4rFp&x#EIYl}2*k$o>CQfqic z`S7|tif;p7KX0_FE6@nRJ{5r(52G!JB#GD?>vRi%6wykN*JOoUpo53Z+oNyCDM(}; zgc$*VC;s<3XDLr?11dea6Vc8Xzc4Z1ek9m-lMacubq6Xvn+PvX0eeu@cBy?>Ve9n{ zXrMNEo9MaaYL?{UX`tBiT~&C~jpMmj*xq}QcHytjPJF#KfAm7=l7v zchr&+FJB4_onFlJm_d@PtWn$0Q* z|0R4d2!!clw&Wh8WP%{@$=(*$Rs#&t9p-I*5_gZt%iM|2o|(?goi2c6AA?jGfOIT| z%%o<_tBO1a#%eRdZZkEWvq4XHJmmF2L-at?2}YZPpvz>TrzcaTXJF_QVDuwk;#Xs) zPh#PBqNR5mpB^Jaqd|~Wa*u@*-dP;9pWMS+0iFv1oROzVB&SoEY$v%~CxvIp6^8CL zzuUv$AZA*;AH=vAC{eO7{CxIxvbi`3TfCsGZ8pLe?-+2#786DH)1Fn~sS=wi+$4*G zI&x21&^SCN9C3~Hrz~~olGW^Of+gaJLpAgN4vHv2-bcBZR_jB9x)o-Oz# zcJKDb)2~-)ZwK4#za`D2Hyk8rteexz920q<$a)k!q0zKKzGE;Z5PZd=;)RalFet(5 z@yIg}VhBhH=*)Ktpi6K{arOjj-q+}hikJ#UML?dMTU+!cfNc< zkCw2)1j@AGV^ON{cMKfi3rrj5-^<^+uAw<;V?uwpP5gD6RoPg+nxAZ|E@bIR*2eC zV^QLrH-%$xvUTom(6Z2M4d1-ha)5)Gk-@EhHLKF@h992^Vof{xD6u)B)6f)cR|Jch0k!vXm$ znPyhZWqe1cY4dqP{YPoQmCq}WZX`NANyTX@GE9^$AUeRwS1znbd#x9JAQN|433g3sDQCM#vi{4qe==5v1HsSyeYa z01#n@p0u@VUhN!65GjQg00MGP+2@%%fLHI&cBm`JiE`BEF^*x}H?)>x()ZD=aFZYf zHn24|Llo|289G z8I4n>ZUD9NSH1(*dtB+%fw(F&D%*>Fwgg+UK=K$HRC5z7gPN5I)yI92g}^Y6%Y#CD z>A0QLZoaq$J3{Hv2@kzmCfp{(c4HA>#I9vRy9nF-T#eCI6NO8kbxm6cU!3&?iR>7# zI-eVW*fyfL!=}J0gqgG?DygpH?TtAwgt^tNPO!tq4n+}$5|&M9s1ZcSv-;rLWc>`? z)_jm^UwyE8aT%XBwIyw|@C!NVXL#B@XFnMlf?-1=ge_i3X6&yC*qn8fZ1*#?6%c*Y zQF@SEkRB$?oyUTAgdMwVmvATB7tGEKluPetUP%^DOU8*J*wyFH*Tc4%l1<=K;*8Co zAhEMoO>MuEyPg1iBd-`TNRD)1tXe9{$_yA!s+SRTAe5w5xGxlNbQjqJ4;vF^6yl9r zYiDN3F-4>23A*ru*HuMX6fRz=?pW)aHtMr;**A)ohlrwiZ;{FNx1-wFJ{w8T=VI{L zPnT;j=toH9tU`a00`&wT@j7}$07x-n`()#~R|9-<;2Vi2^^o~S>014tK{P6;b2Q7>Po%< z2tK=kIzv=KEP7&DTLmROt8V{GkZBn`nS*A10H;3=#y8BUh#t5jlNMLDUl^sVia3hX z7Z+N`F$lHAADDC2FCP;^9<$<-VQHk#*hjXwk*$&38R*+f=Cc zEcd?6J*!#zS&_wFAm6ZTTW8QA=IQ!m(-e^V`cvVGnOm4oi-YaEm5^D>MwoHyr+4uF z0FEuEL2~d3TFVm_^`Ni715y zAc2a%B)###+l=T`WjZ$6sM^D60Yr3rV!B$7f6&NdNF+%#t#h z;hxzeZ%Q*p8TDD_&8RNTwO9rW8oKIlS)2sh0tEGVTbd;)(r3if^|hrBLbu7odNvc& z9v-u3oLg;Ny(r#0Uxu}heOXo6c$4aGrPM5DLH)6nL=uYw^zPb*|47s1JRNr8S&D7tZca1a+ z<&X8Mn6fR}30r_j?b0i?LbohLbaRD*2A4hdnjr#DxIhaWt(`pHW^O~Zuo?-s;3aqD zQYZ`+0`Qe!yTMeW;0b#YMs;pde8rP$+5g(2*AFieO11JF(a-Gi_Dxz` ziuQwNJW3qz9SySW7c|Wa(UafVxDee{H|2d6-t2XQ6{b8esALjDXq1yqz#yghDhD#f zd7HfU;s?&4Adf!^1Vbaa8!Dbij=Ljd&+0$J4=?qRBcAe(TPhwS*_vG!#oPU*^LpUkSDG5vt;^e{Run-kd(9?)RUYja?Of`0YfbZ>OJf;bE!ZaT&IGO4m| z?G&aD(xcr2L|7%#376^MGGxNYgf`mdw#cH)1@ow*zFgdLPlsXF4Jz+Md=}(xtmq*; zW~2z?IMf`Z(exDpI-eg|*4+X&S-SDyKb=^5#Od!S2)NWa_5`I2RGJdyDA+Mpn-cQN zZdFtX^c%V)8gn50hHAm491h+sIoluNFNv*{mXkpY&iz)$d>&0#*<$h&XCw^XWtavt zv}I6E>Tkghgs>%@+_G1Yoovg|bO0%8)_0Q_9AQ>MGX_Tg^h|kSHC7Z9ivM z4HCS)WrVt=1VD1GA{|>bjfc1MquR7vEd=FZ|&TL=b-S6H&TH;8Vh{iSb~vdW!*YY9`6H4_w3DLJ3D z2u(?(njn`dXMHawNnJ2Ctm5I4ha@yOXfoJvxh(_Y2m+_<&GbX-C#ozsCOUWthwaFA z*x~1oQ_K1!`2VFmqDjJ(hRk-Z(~^ME&yU8AH?d@@KZ-EH z6@0k?1+H%;laI2xsS zi5*|-49ql%KxfoerGOB3;O9`X!X>%p(R+Ia{GsHRl7I82{@9``{>k%&IoPmK-i8Gpq31}KxnvJW(h{n3eQzp5ndnusL> zh$oL4%2!!hrB{-903kNJV#i0S6Rw|PMP|D}vABy+&RCenhnWX)XmB*TWpNR9CF0P7 z9B#ZHT1=@4376_}G6zgm<4oaYCM*k>J!6MU$3%9}p=RzkYs$zR>6rd2u90lWFeQ(p zd;9a(%fLn&Z0q0(C`Z?6%4>qaK6TQJ`?t$-#Yq#L1xf_HWG7cDhHo(i%?-j|KZl6Y z*o_5vnaSzKt$x>jwiJMStk?(l-~G#pwX*z35MSx4SW+L6e@w%ndycHWV8AoZ2p!n) z!nmB`7!5|F8Q6_^1*@-&V=fUH+{NVRQI{jrk^K;~ny>~Q6?mqstMgQ~g;$h=m zZ!H?+383zi@l%hLg;eh_ZU5j&-ufc>aF2JfYc=^Z9Dg>J8N#uL6i^{(>z#&v)1c0n zHuP>yyW1GowK&zY82ZIcb%8SWT?r)G9peizOSXIYOlhJrlD8OAnij5@MKkx{wL;yn%(h+cy0wyU~0q z$^t2Df^O6g?YC9rw_lvF;a}V+?5bCtQ16~&A$T^rP(v!raBZq6Qyv!(q}opH0GD)x z^x1a@i0kpia6q)}><20WQ7`1s&#)gGO!{w~Kitdydb?-e%{tQ^zf1_W0AAXV@Hvq9 zxR5m7+3eFk9Cmy>AFRM-OMuoKb88N>!3wqM3q_)Ik_g{lomW@vTirudRDe9d#rfeUw{WJPs{|ED9wp0Ap)Pwe5;cc8{RL zv!^4Nr9X-l-8?hu?gHTjKrSK!sR=8+;_GEEJ2&p#t_R_yaUzPS^6W<>)QcpZ#2@FG z2IsjO=Y{N=D?qP`&8zDF*`l-QKaAVv%AO>#!(n@j9}vy^PY9nJb)Ra?$S?DF+e>&C z(RVH@m~gczQgwe%Uwlb9WhdNaw{0l)fg-q@A81!_$Ke!Vs``KA1 zcWO5=odi*oEAg(il7jQe%-pG5KdAfzC2UA}C?X7ZAAtL9E@i)Ypzw?RS@RwaK0)Lc zFhF;a)eQL(=k{`WGkc>6RlHs5yTjT53ij z%2OBYQnwpa#S2sQFH*harO~fei%C&~N~#n0X!@@nEf#__d=(Q7mFSnnx2H$|C&=&; ztd0F?RzHW{5Hl2!4E9+WULZJ5BaGe99g46Wcu0C!^jxkcB=J|ni(m$2RExASDcdHsc%Sd%Q3Pf_1WvD41*Hh?$ViLauP0uK7x zu2cpO3S^86T+Sla=1|#u@w3+;ye$`!`9T&?PmEZs1VuBWQ#NyzqzQO$cA;)^TPb8v zM;6e;*41ui(9OLJwdmR?Z_^pm-<_6Q6CapU8uC__>sQYHO>ZB|K9pi?h+^%Co4{3} zwZD*CXdFk&e+2K7i~nCCd(z0CA-j(bY&p3Kb^Oi*r^SQEe zhd;L}LHU35k{7C)QYWsO+PNlmPhRs(W+C^osS*0;ANRAB#TNkxvNuIq)sJiX#j(^2 z_jILQO*tjI5a8?{^iH& z)c(Uvn(sR=t#pWi@TA=UXkcPwWRT-xP)q_G_SrDV(P}J4L$UI;$n(IF6Z5bO`P$7* znYh!#ZB}?tf0mj1t?v3I3S{fgw;t|VKup1^Cy51qcl=XAmm)K#fgSR`;4Jp);qSEk zV9ZN-Xu6sS3bf5P>Cav~w!E>HeK{F`v19vHt~6YjRTrYG`Mij5Z+X04 z%-nxG-@xwSXUyU^5M*{~@M^zI1Z`KdCHj`tvnO`6HFG3h^|y0+*yYI?DZN!tze$F@ zTI?4$S?X-&u)XNhF?K1oy^H?pF;B6))^C?S(6sjT2cl}2_uRgizVr`jK#&CK$li)P zDCT=;5e6e)k1-W$xjrx?+9garU%{&efImFjs!N5U13LpCw#He8NYi8j&|+p2o`=&+ z<81gxf7JMfk<}0oqqrh|xqf2o)=Ac~s@WUoe@>Hi4Zy>ANQLj~XtQpD)5H{{iC1r} z#4BI$c$~ez#r|PMxfjhod@D7&iDED}N(39gj{9Ett9+nWClMo@!klQ6kfqEMx&o8Mf!*7T%l>6w*5HV99ZfZGO zC0W{1u2JL+rUh16Y2HYBWhCP)+b>#u1=6f`egES>hJ1FfTlJnaT<9pW^OR}sFRn^J z-x${mU`*NkFlB_mS4=!wpvUZ}5s3=JU}MqP0}6ImedaW`OQWfu9>-HS*;A$n_Cw;6 z5^Fd0Qqn(C;qDyHi?bg=(d+O?s`6%KBuiHpJ&;XA_tKv4k{*dcAkoKJqu3!7Mp|%3 zu{pGp*j^O}@{W*fU%2B!w?t6hV=xaO8)9Fri=*bJVUD+n;fQlepvgcnyZ}1zYc{Wf zA_p^+QvsWBl`lGspBWmVx8S=jhO!42j&NG1$}_0R9tjgOkQ<~xYV}6?4~3UCre^jj zC0H`0OS9IHW+P{%=7*jr7au)z2+C!Kb5OWZx#2HMa`_|+e9COKM6fHLQUnU9a!D$E zf!{;bMUt&f1~GSWdrgCFS+HifrC6Um*e1zBrp|z{lI(I^3!{yLct>ht0n%?U%9P%a zo7`0tBd z6d|75Ac7SLqi1YLk8)OH4`jO#&R4PQq5}x{LAhyfD#WYiepAUQkizZq%sfY2RC2e% znBW$P1naAQ)ApqsdXb*@i&`UAalOGjiiFc?tmsD*EY9PI ztj=xG8mz=`kXX7rw&Q7M;HUAYe6xner{J73<;)7nd7Y}V(&d*`=4(;=8jWU*Ae?b z<=E9%^b2{B-g!gWo~dVQ+*cR44`l5LWM@z0^sE6cAQ-XB~_@jQtQ1^b-VHX37M(s5)4UR! zzDvL_epCMQ&1Eg$H63OJpdLa`n;axCgMS(GtULwX&yjXJL7YDI$Ln?8(-bnhMC0pR znavF8-Av|_NS(b*wY|(WzsG+!6Jmdqs2^ml-TZ>cmg^j5n+j2z_E&@0ve#WB%T?-C zsZSQR5Zg_f$TK)d^-h0XxiGq+p`k>Y7_fXI*K%vUFdqE1x1O`JTB;vbpk zXlLd?CXpZi9R{$v;Mbrg#nT(NzbY_)W5ri6?RZ_VaThLIKJR=}Xd(OxT)zwkhAcUL zVB8Kl*}-#`pP^mz^Ms`yeE1b&4s$cGlj~s(v}aFG+1ut@8dH%d_-Uc_%BX$iG@!Sn z^xW$`>>}vlmlbKz_`z*RSZUhx)~~16*Hgs<-x(W6nDncLw68kKUO8pT1W;?Z86nHE zXFQ?8bM)`%A_qN3nDnE;i`XFV`5s(~^veQ#GBAM;A+ViKo|xQ6FVIhJG}?+xi7&W| zg3zZu1w@sx=ovcNW*<(gd;z&zD{YVVwj<=%clJ;MCyZHV!1>>Xl$rjmq{Y!eApjJ6 zj9^a11KDJzMkKJYODURc6gOH8-)azve7X_$Oa!3GPH`WMPdzz;ZNoZSrEuj9OdK;3 zhr@DnNb?t6f_yauvu*uJv)ZZcbOlR9wDdY}rO8Ly`~0ZN368F5msxJ&=*KRHvQ@^j zu>a^GG)r=+XWl@0r~FtE1f~VdKa0!=6n|^dCR)0e)**2~4bth9BvPF33=>iXMK=dH zD=C3F_+#wD+I7s|3O&ODeb?7_7)HU?FdT{*Qm|1#wU=;}kKGmY8AQ|I!lX{mjzgnq ze-I2SJpeEH!aOd|k?Oj&NMlQkUr>aVC?-f5H(eOw2as2@8QN3-_@$lgAZBQk%gUr= zsqDyn=eYi+9a3BD=y<#2_qK!Goa>PFQZ1Wt+*BjG3NzETpr-zvZ^_=fT*EqI%#eyD zE_K_D($f7NseytXe&eu0^?rbM;fY*$YqK>1{o7X$&Hz#(N(!S^-UvXci~Z-)_>G>981$kT<-<&~m&4KIpzllF7@DmP5L3j!%I$ra z6=~N|uqCNa&Y5CH01F8T4FS;==#`LmRGsp{3Q1_R_K>k+cE+kps=ny1B_#=T&X+Rg zkKLq%KORCGXVkn20pg}$CBmFA@~SllU7b@c@=_51AW52z9S9#a8b+mes2uak)ZSQ@ z8M?ZvJGhuZ4O(iI?H(iCzaRnQd3#7DMB=b=Es`8D zl(k-LMH?CM1TP3H0)X7kUP(6jz&5OLUOj--&sS*?#iq{Y7P@st^7w1J? zkfxBcZX72UHH=m_XN)p8%T*OOlncryuf_$dsj7Y1leHm7u^;Q}FqMdPPT)Xf>Yh{$ z=5e_BNOOR%q9ODnI;H+C#Wm9uF7QzWHd>gpncpf#^b&cvw6m_rJW|#0#`iP#_|_gCQ?_x+zz;TgK4X0Oy3R6r3i0U2B6Rw&uC&V~u8FZm z%9P|_OcP;hB?8W}PU`6VZ)Aq7{=1dLjw#@GV#?09G*ze$6Gez{We!I%J#J8}IAV}@ zBy7k(j%|?&&HmJ-StKxfyGXAvSnKHWS{!$4?^#89xc0qq$u@d(BDl(m<+10Le#}_k z5_PoZ!#w@0CQkX1R6Ll1N#U@Zk&Z$Y@oFH)E{S>$;KKuwCj?c);h71XJj*0r=;Joril?_MoY#DQHURh3-GlC5Gl z>G#!iz0hq5{RQ987e80?vyhamRSt@X_`hs2yy#lIw#5=U=LzeK7qEY@4Cno4lfgh- z{b$otlhme2m}Zb*Aj+$7zps8Ky`Lf7t8>DXsc6i;KX+$MI<;_w|KL2j z2i%`x+4LW-c|JFLzhA_cJ7M2JcH$^Pls@jjzm{_R+NFH{DBx$3aJcgHN#Ju&oR-&J zUoR|M;Ut2Dl!GG8s9pWYYBzTq(}LXCTEnpwMcjU*p=XGFAslh<-Wa7EwVA4ys;O3~ z#MAi@I@Ysf@Rmm)jfXU_;b*g9v9t6Pmd7(I5ayO1utf`?%&GuLv4`%L@`_PZ!%qdeNv2zvPcaD@1ZWP$;^}Y zJX1v2!+cX&oYOo&KUEf?5+avNk$#q@UH6$Jx{(nr<%XtkX2jh`{W&x)U^O%4NE9StBeBH*Had`UDo*!BYff06~TRfLO@(4R}K z$c@jEUGvT~fI>;O%D3*opJh4=HPbA@7r)-narr`Z!uY);alk}UB3<{Z62;yHl~K(3 zO>$Nn|Ms{LxWfGSfp9Owfgsmr;6*46PNleqaQo)PXMszD+j(Lp>;kYLkW^{@CWS@q zLHfU_`ZC5i2wy*cHgV&V_`tYdf<3rzhQj2*HK6}?1#`#J+ap+Z`72u4E={yfEFt&u zH&xOeb}L5sHs06r<4{r=2pbsH6G)Co`wX9h$s;jMNyRu$El|96?w$s5gBvTNnOiQ} zLE+QFI7M)>2j>qbMQ{Red&q!%>b^67WSVpb*(MpB$}h-T1+VZMCJ|V2DGt|@R&x5J zf@j2q?o=EpjWyC_i786cM_$Y(xO$=F|5dJgh~UsdSI)A2BbX@039zll724!)1n>v(zzB&<7`xaw8W)8%=Ix6&^ciT=>Iw0)Et0 z@xpt1>AGrQk!i6%%vqPEdNUz^avq27^@t44rO4Z6%Mw>O(yBdba(YoQ7%8HOxe4aR z)a9LH@GA60Ap^c@sw)FzRyv&kqDX&jj7u3-MSQoZ`{b@Ck|5zp47kZb!GOR6^8$O-LXwB%HF}Vf&EKBhzIQ*#_w*-JQ}V z$p?G+QhlUF>>e6!Kmi~Cv{lI&w|DDR0#!V)s=w`Pro(iy4ij}!5e%<}4;LtQ)CP=Y zyT!m{XGCW*+~^tUCNc5BC^NjdSs1!CkfhjAaRijQ6{wyT?dPVqTY9PcsW-Z%eT;zc)811-NtCxjLN&pu*pAvXK;Qdc3iChBS#@0+y#`IGNRP$8j)X0k zYX5seqR8%AYE1Eit8#blt%KC`PaKr9TZ}%^N<&Kvv*6pn_D`e@$#RYBB1Y>^Yz*)( zHZg089Q=z-vZGn^;z{iS{XCukBLzn#anx3xhM22`B^PzVCveC+wb#qjWAi<^@C=-i znC9zgJW4hB;HFfMFPxP4F(Ivmm10|g-Lun`NWQz5QXVRqT}8?yp!JJ{p5+CD&KCQ5 zyEcV6USvfJURZgH0aJXo<+{a6F39vrkq1NQiQ01Wg*l((VCW+&l zeDVy`kk!7~Lc)#TwWF_hCU>kNl!zlbf)$QU({5q$G;iDc!sR6z!_al>T*TQ-! zwG|fvaryZ*UHIgggRJgj``-0S?<=aWR!85p-}{Nz8fXgwK0>+h0=tA0b^MabwY1_5 zSd`L^NJ-CsTb(}r9=(VEu{l~eDZ*`;tAF=|L)cs0zpOZR;yH8D21IBh`^3Fa$-Vl1 z+W&V9|JfAaoGZ6K8N0@#465cy_RW`v!CwG@4S8z~g$|5j^A@H28U=QX${ZSIqn}T^ zMKtFvmMnzk9gJ341Rr{!=$N3)*`O+!p{9GFAxNU>gkiK554ekh)EW|;Gw)Kqq_@_pitq&a>G<^mlaCt#ND11g2Q7oIUST)Swx0{K;J(P8bzoA111jxtu zt4uQyPi`-8apQ2c61z2NkNDnG(`Qr37Lw2zl4DHGLcUW&)TuAA^<=+OH$?=Z2|BC1|j#{pSo{))QGIRD4P{gH(pX7P$ zp>^DbdwfYsNN7WFU6qI{A_PO?a$9nQ7lMAiMgoGciXbcqGHE$fg2O~yPxToQ z?<<`t>c)mhyh$c5{NqFdrUl#oy=V7dz32b`S-3&Y^uMz36(>LD7DJ#L1^l%j(W-5> zcY6^2;Y+F334=Oz>kw-eXWc(Me*&;{ouAkHJdZ;d=G73UAo50lOr3r;%u7&8 zS7pzddh78YRCt4&@DV|ekGu`opNQ^%zV9||OgQCN#BCU4Y08d^P-IPqzHRYc*ng)x zd1$vzHZ-I>J$>rsxR4Xd{npAN(sY``V1hmih)QG>!meq5eaO_?QW9$6`=@MU;abm4pAHHs&8_GQ zDyMjy`l{YySr)3|lZIF?Xrtw5=(>1>GT|_Gb}HmSqm!heA!vS{dR2GoZ&Y2^8<)PMh=xJL*s>0m+%*&=;O6l#{K zLJ|Jv``Pz^FA%Y}bWA-+JmqOX^c*(oVEPNvukZ{gV^oy z<{%;{kRZ;IxHi9cx`IvI3rskUb8&S3c z1&JNN;p5BMtTX@nZ==?$>!EJA~G4;!3|tKp+p1JKUEiMOdmJphPS|0#^<*@;d*szNH? zriSIRi7WNKiKIA9h$TE#@lSIaE~_?VGZZxh6{3jP;U5?LlG>0`gy1M31ItwbHn@Qh z)1`{{(E>0+-(B_a&#Ck7kWJk8)SL(+yV491co?Ok3ACpGOO{8*Ldx~8V@W_9>vA=uQ`1q;61_7g1 zFsMn#U_^gO5sx8GDUDadS{eLVp&l%gm|C2v8569*7Oq!IRNSE(3un&Z&|#6kV7Zp) zYegnmJtWqFDZ&xTBV*N9RoreWT-$d^AtgD1!7gE*=xpb$V6ySKl;3Yhk41j+q^{-Pi zTD$MKqijkd52ZBI110bch3cS$tsJfk_xoN*Wj8UV524pkM%2FFY?eu_&DfKC02k+eC#Ln@hb9X5 z3w8}k67AY`Z$4opUm+yP=}~OBmn7?(5u~~m7Z0fS*z-O25bqaV-MoIm|Y zUEI}pQ|A89-t&})?r~HBXhh)UHv$?D)gu{k?!HAqgoqiQL>=reFa^m3$ z@2E8Q@f_efYWJyR6Iz2P$C`e>#wS&clfIW^7@1N1n!cuv0Mz+vOCzRrTF#h_9Kz_! zm)RvXcV07*u*u)vA;Fj%@o-<-C4;=x(j<+ zbG*(A%i5yQPu_`1CNrk$ATEwqAzP_(m?#tz`r{2Q;%#gq=3TOO;3N^2c(Q$BSk* z=k}+fg{O9n3Ev?|xUOipO_;AvORM@Gsxj-UW%h%)Uk6*AONS{z;q_$X znL=tOLNVt;U2zC5SSFAt;|e#4w>_rK!U(qL@PUXCdmY-<0suHCX!PLn} z1&G_EnR!&aOGbP#W^cU`VygD*Yf|Z%b%xW#D#8^>7itiX+V1@x@YfVeLczGX?uY?p zk$HUZsESO$GP=M4HV)Z&=K7T*Ssi9Ln}|k&(=wz~b9#fZo{^MuW1SI^60dvP6p9VF z#-ZI*3P#+9_eua5Q_;OAl_5vl(qRlAMxTfxi6;e8u6jzkaanUSvF z1748ZD$&@9ThNAJ&cNf)LY3kv=f)8Mc*!iTjo`tE)1FX@rYCyHz-rUxvTOF%L_PtY zwI1(w0wj0&YMyKhu>^aTrZk7<8iCfDpSG;_#Bq)pFX064fF@a+Eg6Y6ftFgPJDS%+ z~LpRAM#0rAu>;{p4OOS!69 zuy6WNr6wh7xaAp+I~cf4?p(1VrmKks@uOzb#QsG066<-W^DM4_^_=xx%mQRun#URtoTDs6ikig{t-QR zlN5-HX6lCvWW!b?#e|cRfZL=@f%PYFi9vekv#S{WIu!43nR;r{9O#SL7`SMiof3I0Wi zmTxcl_c>F}UJ=~AdJp$%4(_$2iCC@1avXbVXko& zL1k?~Xp{LC=w6;TzaOqtW#TpXeFrfxDei3%kauay^%%mL52;21a@>(vR&S0R5_$5? z%4co?V4ksh)n;DJ5q=)G4;s^jj9Y6YbwO*}uJmwpS->^oqi(g$d;%O|a*c2nmhR>F z4YImO4Glrow3smYG**q|)NmY~Lq+P5O+3Ca8hO2V$0%|cS>d8m3P>Mw-3{4!G#k7x zBb>sv84qC@1!*|)CO?`sucyX*665@?%2xfiJNzgjJ#~%kd~h~JS{F}mgAwAi$#%HT zysNGZm2O(V+54zNQRZ%i-Em`d;nsC1t&#D@>`H%$9%c-MELoy9|E z$i2Hla20E*{lU}%%LoluAqK=L_^Y5a-Z~v!Q^#w)`N#%;im|N)1}(ErS5rW|dZPh; z(}rNj{CI=N&THXtQv{VN<>cd*eWS^8JIl$xFv#!qmA`$&;eK2I$F8nLmZF@?La21S zMWg;krvbQOS^Yo}q1Lb!W@V2^NN;Y9({87@Kqh4*<8n{6gDBN|=UQ~*x^pNs8$(TR z{fr{k12#UJ2WzxtxS&YrrPcmf*#1f<_|bMKB#&M>v|RidhV}!>B8Rb z9lZg*-m16Yon00`vH@i^7mJV`5H!gHlL`SM!PJM{aJghe(-;Kj{tA&_E z3Np`rt3Wm0ov$sE;JK4%%eOpl!q|?hjWtSG#*iF+XX)0y6Z4vqs9e}*OS5@~`G>a+ znq}HDFbOM(#==K0oK@?+F=bjo?ED)epVI?TiB#0ARHs&OSd6|^CS_?_3Xvk!xyQEe zt5nfp>Z>66U_TxGsA$nR#^of&VCU4H>v;tq{dUd#EQ^z-+f?h)RDTqpS*hpF zZvN@lVS_?abE;iyFj5Oe3tE}2^Zr@4vWb$tOO<`-^myuLaMNJ=+uIE5J%=S#4nm`S zTH`9NcMh@JRshcaIS%=hb?_9?=2d&dRZg~4J;Nm@ zN)|UtUA~{5rayDD+650rl1uQmx8RdYGcLVTf%ZXN1ywsz2v+C6lp+wSS7#S>rjEa- z==Cp-;KO*oiH)kRTa`S4H?A*uLIpyMtJ>pSf~Ej1`YLEqX|Ks;`c7u}wLMFy?q4PZ z-=EH46wg>?NgB*7>sywgP&R$bPcTT})k(xc>(hBnV-{wBwv_Y?q*j(i>d7xJ3|YCW zh{-(E0{TG?OVZ8#gmd3}`>JYf8&RpGs!v-pQD*Vv%4i#@#Q$)lLyPa5Dc@o3nA2af zH}VRb_Dzs{NBS%`gz7YfbU?(cl`AFJ~bM^&G&x#=# z7%K9i#Y{-&@rP6|Hg_%?y(|};*_s)_T0?KOKa{!X$GZ8}S`opTnW=~cKO*g}1+5M? z$fu2NNzC?|a7%@3r=9}N3e=iFd`tIxtC7I8$AWf_Y$rvW5MOL->8Ew`?<6`rTw+}Q zBXFzV?=2c|4;(6?kds^ZEi!-s;-ttqm!v zjl8pORJWXg55zxzZo+MCnxMVT$K))U-g-&Qv_Y|TYqQ(UxE;a2Jyo+q{o-YJ+m1>2 z&VI|T32?VHonJt84^%D++@uP2!ci+FuxQwut57I{Hfc4AzbzQ;bHEMuqTq9lceWUc+JzR_snn7 z_h{^IzAZ76I^%iOz8|ev6zU)f^ym8=ytE$vJ1~Y8F?1v!2{E|W4A)1CoD2;nmYOCF zQz4ZV%R-gCzC0b)CMOzE{Xv)jW=s*bLIkV9>oY+_)inBHAP9OG2qJkvvD*0G{Y!_F z$C~$Fis=^D_`UigsfQhWsfUya(td8HAE+`=q!9f8 z)D4vB-{IF)DlFb|o?5=X^@}n+^)he8{zf>uz>&E^;gzT$VS4N;l0o+>wte^Jk%~G) zwQAnStTB^)6#ag-``e{(024(;?9l$TsCfMw{==^a?$2D*Daw`e_JXSzUSqZsKRa@Ey%cG=EoYs+1jrGg5%BmCT-3vYDs`e?T?C)>K}JM3%SE zI;;Mu&n=JLywr~kr7JhED)Kn>9V_0hKQ*;*n)HpsxRJ0rnZB2KaKMe`T<@w06S@btP zOBzYrng;2TBbO6}nGS&k1mYOfWx;V^Qv*4w7_(v1ERMpm#yP62>SdO(e7R_-@zBou|=xWZ#{#2c#%!#C_Q zu%tAuS7mPXDUzR+F40Vh2P0H9Q&IH>f-P&?vOg5fY|)k?{xADez5*!(7W}T?x&2$f zH;I)crTPEgpR)f~f9n4JphvJ#7s>WJrrrQvlinwg1!iEMY4#F|#M3_zy#MCO zO}|SWG2?w$GvUWe9Nuisb}V0X{IKTat)J6hf4bepTdzRL*tJ$5I{qGMwhs?K8dRc* zVS^PNmB8qhP_4rcv&98<%s-dKh6H`LC1D!5-`UXH6j4r~*0&f@lwz(jxc<7@Irls7 zD`d`=uwE@hI#YGa>ES&>uv0<7KIG-yD{i-JUxH&&0Br)|4+Yf}#NL;dlnt)CMWP^A zMC)7>_1*8UtwCk@Ws$BtwEdR7rRf!{D-I;sNUWpY6k$p(dj!aMJ#KyKKazYIBU#O_ z#lJpH-2recIjR-^Q-6wooIt@ydpyi?&1C{5ch_$srK;sc?(E3AL{0LFDVG|7nU333 zG>q#qHOENkHdgev<%}eHo9s$)T{&*Ep^c@3q+Ir)xvF zrbY^*_Lz|&bUzfT+(NNfswJ#^=h&K_YiX*dzrU>cTHNDZE2l8*T068oWydLNzGNl> zrH`;OC;Iuz$2_y1Gj<2XN$^dM-cep+W)`+gV%E}8+nXIZC*HKZtiL?D308kzL>zMt zVC%qiE0KMPBujm4*gtI21@Dq0a+S2QqiiROcZ>TbrCS~K{7HM5p;xGKqPw(J7OLFZoo8``a z=bX!HeiuaL-Et}1sSxqhPtez@Sxv>Elt_aYOqn#+ond;(4AJ-^a`5P})W+Z@r0Ifv zTRPhC0oWmt?6X7uy9;rl^$sV)@-shQYSpy8A2Hgt!V^S;?k`kM%R3_1|#VzHv&F8*TU$;it&{gRf|p4kGnQakOF2eeg-u2; zX^vnFmrPl$gVM}pz6bD%sQ=%O`Rw7*cH|&l@tgr)#6pTqP2xw;s7u9g-J;ehOPW&;rdGc& z?sQ_pz%+ESAZudaF3F1wuR!A{E_|NW^!f^y^SqsI1sr5ed65es%#9j8i*2r?t@f*6 z=eM8jbc%%KgDx%KkaD6+mc&-q1EIN*`X~s@D#KCEB(RmMnc$|PKQ4M8eUztJ?*OQjL%O)V+PcND$TY%Dfnic(Nh6a+~p9d2t4deOL3wKOpBQXWM}1d-g2#pY)u2l|3L80 zIyo!<dk3oD513XrW- zZjWa=@BZ4wt@xI!J^}H$(Q;amTElfw;W~u{9>$q<&!@&535w zo_@E?{$g1QLyqM`^-f4_E!&P9$6!`a3h=N3yqiaKl=|kvv{a(1yvVwWL!CSLO8M)U z@Hd%k-%$7!Lv}~Uvp?&=^9{qrnO~XYA62T)OeXaU(4sxd3#6-&@a4%s=i!K50^~== zhE zeFbk;f4;j!hU?M8fj2e6e1Ua>@MpqEnc2ryt!2#-hj=oNx=azd@<6eJsJLMyu5MNM zowE2oZfjTQWctPXfyGoVNzDfl#6G=&UPL9mkkkk&J26&!*zXABN86-9;rK8@g+u4o zwU;iGKEGhJGh-lvjQ?`bC**d|sJ4=3=0nTqk*JX#a$|YeL$(6;9E-o!_cP0t0C&$D z71&ug9NBe3XUf$2`7G~d$ahlz;-&aU5fLZC8|qG9QzPHvrgO{Z&e_MK!_~Ty`tGC| z19ASTOd*By!*!#v!t%sc#CQ9|fm$T>WEw<2vGfRqO{ez4rJ7v&i*7Vp)zR7Q|0S-o zUk{kr?%9ug;Q7GQj^BHkv}4NtGiWRP@^g`d?Qs9bSZcfk1QIl_M4@O~omYm|KD45Y zapB;o41?l=~00xLx83&)s3ivhoah`OF&+K<|CJQ{O?!SF`Kz46$;8takHtvY_ z%6~Nsze|`WE|yB~&1RSq=Dfi<`0$l%M>Fx&XU^5ajjH#4KO%jtMemz$4JK#&

10 zn9&~*X&-s)&W=K=r0Rd-?$}jmn5=%Q=1ybk?)s>QPTGbm7xd-d`ct+aQOq)!s=0)C z?Usr7(b73Hb=zI_Rwyr;y-c8rd?Q{hHMMBzDa1xxX?(`hLgGu8 zFo!%qqc2PnfpU7fQ`^3CBFTdxyBqeafJ>#IUh8{}K zh{F$u)^o?Q^K1(9b}IAL8uKTYkCNw)i65~V*Rd|QuzB{d&2KB=PenMRPH;v~R3}d2 zQ%@%6PH~p{xdwh-cH&;)w%BgGv)op3L_BM-+^uMbHUiH3$}ez{zd`U>aq$Qmu`d~* zsQ>^zurxi%px8x#9IgW8b;#fZemMOC`Nacc(K-?Upf}3F7$wbOYD9ple;H2l_ieR0f z9x7;|56GNq+k-hIfCW*V&^)fetOH~U#T*+UsdZ>l6L5t$k!t{xUJy1-$J6osrIOhl zy{q_@AHR8aVI7Ruga+yPf6*Q?{#Scw-MQnBuA*$=%=-^D@#EeRQ-26+XIE?^Ja;3VbgUyig3BdDH5BNAhQD;AJTBcUFdN4Za6{ zS*(7LpP-M7yWe#)`P9BBOn%_);W1l$pc_`>c5kh`?Cjj~&Fv(751w?OT$Utd*g4S# zVg2GkRfXnN#o}qtvw!~S% zc;-r|Jn9PeWsTBe{KPzVh6pKWjUkrQ;IZ%6gRb^F|I{u*|Ir?z8OFy9rxH5{lDdMi zH8U7<`0OU!#hBfbVXj?v;~H&th>n*TLBU&;%S(-C1kb<#5`S+E{=c&F z+^z4T?6fsM-AHroN51iv+9%2BsxyAIdBK$Vf9fg{17fy2W{gt(M#{(XTf1z>s=36@ zpW4sMRS}x6#1r|a?j#}yb38xbv>dCM;Pk$K=)H_%%A?-zd__c7(Pa{tt#%08(_JNd z2TB6q=+S8&Uc>fuB)63IgS4fHN?y_uS@tgIi;yZ6k- zvHy}pxP%uo=TyM?a=E9L)NKp68j+Ub*q&MbT1yM^@Ca+nv2tP{-OfK`Lrb<$ zY3y0DWtYMiGOp*{vAdMH1hpBk47f^$5%UtREV)xnShL!jxn=#EY8S2lp1V#umZ^6o zQ@b!By9ucRurY>sD4BxYtxB8gKXet!Bl=|br@UJ(%|v~*`WQev9$A**2x~vvY}OiV z>XE7(rnXcZ->cV=X%yvm6J5ObT4Q9cV{WST3<>*oN-U?x?g@TuTsHDu_I1%sI$T3~ zt5g=J+W2mSI&NSD@^%`J!{~+)P7T7(3bqn#twIS?1_rvUbP{zdGshO5`WJpHTWId*6{oB@HZp;HrWYiSh^FRWs^=$LG7*Lw6#?YlwxUNf6G-EPu9 zSs5$%QWi~rGL618mf2)zjQ=GB=Rjw?kItNa^lElA5YejkL#2z}a9?j4a@p0iuV2}|j!!5sn-JD6t0fvDCF zQ>tHA9+{wTUhHuE1lFb|6)?il|zc^jpPImS~nK8As5?2L% zEISriQ5G~}2%{#h=e68NOmkZEd?i*FrZSVys26AZ|ISSw@mhLvw z^((LEEY$g)v9GetxS18BXy-W+a&4kI>Z{G@5sBZ{RdmT=oa}(w_tuFpb!ABu3Y>zdT4Qv(xv3d8+^9fXBbQ9rt|u=8Dbk$AL`T=bFZJL2+WPXd#;Q!63(!UHc^!L>BKgX~C1~Rkc#{NL;b@;WD#R+|$P3@O5 z-gx=2j3zmBRCGKh+S=mtK-)Zrk*fFVc)8U)hN5lwNALx=Efb6RZvUuBlZaguGs4L3 zrV)u<7UdE26cAD}mQ{t1+KE4&p(alYkKM-5SH(*8Ttlo^6A(~D) z0Y@*SxU6b~2ZnC8PeiOxpwG=mka`_5p$(( z8aboNcZGq>a%a%ifi)0(Qy9oBOUG^G&yjIt#( z;CPC|f47B2KBtjGBC(LD^IgX={l4x)L)_(UJYveg@Vlx1ercTB6Hl1EJp-xFX>&?x z&ZWsHDr*w9vEqYam&SP}l+Jq9-Xu3zQc~4}C8f*UC$n+8zt4?y|2#LIwot@^D%;3n z&B8vAU{Od~tq~gM`xs?#{?itg;kfSoFYEO;lJS3oRtM@Iyu@-LbHtEy;ta3+#ZcaN z#A`=t+0zO@l5%Bxjq)Clu9A1Wml{;S4D!*y!zl!a5VQXHb@kcFDlDCtX9>XEzrR&) zYZC#fccKPAb-@tc3D)oW8u%~MB~&feVXw{)9XU9B75hFfH02zkd zQuA1tG;*`p{)&BL2VW5;p;jm|)6gLl(Ci(Rs`mU{p0n}dJyoLByo(1Gl9bD{*1J&3 zTSw~B8G&qVMNPta?Vb6T%F27b)Nk_kABhbWU4sqVaKg|@{4 zL=;INfFJ&V2@GNn9TorpGDTvnDq+*~W;Eo=9MqyHkr|fnR8ppIv5J) zL#M!TEnN(K|3M-2)I~59%7^A)2`g`tU`)@AMLt|dfm}*(Tzl5z)l@+UzxepK*2ao& zOvDeuZ(z+w;A~Bs?D?KlW!U?qWQqhIY!GAVjO8BkNqWSfGCN~fYX<(cDFmNTL_uGf zHIL55dFRxZp?7Xd!J(F_zuJIhyMP@1zWz;|hpGiI^@gH5c*0)=^1PRVb!sTGFScA0P;OTq#)IR`b-6=>;tdBpR z(bEw-qeFf7eBskq@;QSsGe^$BPeu`fXtMhb(5V%we^{?oV$=;dWex{P`QNThMnh0Z znM+|VsrTBLz#^0}+FIGg(D`XQw+0+*RTMqIXS9z($kV%Sk(w3B$rex1(xxt_zsYWfC zDHBd=oyWh9_9U`eRN@6v$p@8s-IA*tA6zhe4-AkF_na+;l?;3j2@YEPY3@@&nlO?a z`07YXP#$(n#uq$G%jnR>-ZnCXO+6+3vz*qcly=>UjkhPg_kf$@-VxvpR$y*QjNAIy zXRiySb#2YO}K+Yy>u6Dhl`5nHLSXX_Xblon!9ZF+=Sp0I%;BZ+Ir7%l1mmWN# zRuE<7wpgo!NV|ECE1R4-aS(>UDT>2890lcw#U#Urh+P-d4V3%WjW=q!~sP^;^{RM>MGpGQEr$TH52WO+xka+Y7qy^f#z%>+afi^SBLe;^!^v1QCL}x$A>! z57t@>Lgs@MtsXfSPg!Imre#kO5FL%{+U|#zR}EoP&n4Uv#rA1(OqLFwzU7E9$vF!1 zXQ>oZEv~P)7X!6XbsXXGjow2)zNr*2*~t;OudgC!`&GU>hSsKB4`hwWXmWwSi?V!J zafAk_^6C#5%$ieKGL}@D^cTW7W!W7PF^3uKg8_{qKn|)EtcI8mJ&Ht~PtJDBNNrES zv7EN2Q)_a6S+5_FA4XDy-wp2(neC>{C1-ako*)U=`9*Xbi>+3B@{!a>%YI@@X}+B} z56SnUy6OH%{p?V4*TTI&@nvw>!D}w}g@44{>iT4tx5?T16U-K-m}QgE^!eM<+oiRT z5AK+pQ-Z9ocMRBH`R0rt>j-~&^Cy1MnMCqi2TX73D~7n(a`vJ8Oj7VtX-a2{Eju3?Bs;oX8syxuD3sjaYz7JUAXAI2SUw;>V2bGXSVs_5%5`0sD%Zm;;A= zVC(Uk`vl?g^%Cb}XMIc;GG>=jIpc+RQ*He%NAs;iyE_+UbEBm6q*m&ILQ7G0%h4fb zFFrBLMgjw!mA#t;Tk-cQ)T+1@8%w|elg_kU-w$L1#$7^CRJvra&i#A9#AOOH%zAy< zi^*NzNH;vvd)!#3vgth3H930JFmZIT_S0qeXX61j9m_B5RyL=OUr7Tp7w3WTaRp~d zh3wab@!5r2lKQ*3qi8`zNmO_+Xf-b*zAG!BUnpabI2*1NJXio<$+osV*B=7qn&R6~ zFxd#c-{`BuY0@XEN_dL@iG`gok5N7vOT2l+lcEYncKAZnm%Z(*r|j&*(%2=z=%mYZ zzuV`ahm*^AZXTgxTECOIZ&trwm@hKGLRdY3IUz_g{g?Hc?Rg{#>aA{rrAYW3-GX&_ zLe+Rgb$Le3bNOj(s3CZ6x-f)bFN_N0B&cEp+S;R#t(-otr$k-w@2!`& zbxVUrv$_4?ZvM%=yNGCZ{4}NH-5`qPXwY@JmgBmu8S=l4-T&8{N#XFc9UI>jW1SC- zZG+C$9cv$i1?MNdyE^O7{m0L50zbXE_7I4hDR^gnuI8Hz_c+lAV`%WcGT*6 z>2%0lB5TCbjm${M8I(D4>|x-rkfB?=aK!r~z@)~iZOP_-H9kMmxnRY){RQ;Az`Jka z>h_1fee%Zk{>r*eMqb{Im5<(>5Rqe`tFf8Ra$0(sai?olmmSxm|4jDxvhE}9l^2h( z-=Bltz4`KAHj_BLx=4^D`+R>%lv+l#9G>cHZzX=M~aGaL{CcQiq34<#k9B7SFU`^oVI*M!GSsb0PZ}4dOtOoQ6ZE!%&ovSxWh0> zqu7BK!i?CQo-YgGNVmFqQT2`A>gG=w)_x_|?;{TMm2L%#l zJ;p^VtoPW`pM`st2&HY>#OWwtBnb#?Zu#N%y_tLbN-o!3aP{%j`yj@{N>6D^H*O9zgz z<{wpdK3$5d(nD|AoDEmLcXQ{Q=>LNs40FZ6O^;NQ+zW2|YfXsPYv+f&a@Bf(4EjauSN$(NC1jXwq zwLA7$T)JH+ratNm6)5q?kJ!C@pV#opuU5G0V?d4YO7-ox(I7zCrozjeogaT10zKr$h%Vc>`9&C9tvZ#N zc2g!ISr$X*uibXlEsaTK)hn`Ye$xD_ArQA$*7@iGjP{EZt}UPLn5S0A9$~O|jDQ!C zM_(=!Knup@LP!#tf~CnCmg=?-0F>Ps!_1#LXG?zU<<}zfCjn%!a9?6(TfKlVNv{Hn zx#YA2#sDQR))bVxU(XS1Yd|y-rW^h1>WK(~xngMhdDbzdDY*%zTFqI?QqYUT)MI4Q zi6sxsFpEm1W7VCwWR^aYZ0*WM?cgJ)+ zOUTH)OI?81(4!iP$4F@Lh0&bozDLZf!zU$!FLdb0(AY3hOjAZ!Rc8-zY}0MO_YjZG zJhsNq5(Q-nNz9M=P_bQoj+Y7g7T(v#0V$HfjQ+&P4Did9Om@S}g1W0^{`k`nP+9&j zgq2H48zFa0h~lgMakyM~jiAcP9dxGCZ@K&lZWC^+Vm9M=#ae&GZn1-9$AXq;jVaul z*N1GRg|`XG8V1VX?NoPfxri4Zm#!}s4^PPZN|n$d&Ek`x14%e!VK_~r9MTU>O5nM` zgz8|mtXpW6HmAgBw3b9FUlatY{|ij=lVO@*Eg2f5M5v|pkj1`dMa0*vV=%6NB4b;p zU6=Ms@*CY$$q#jTJH0P=^qjPW?HTju#)~8VZyb=`W#Y=TL3z7i3LnuwFRZ0up9lsI z_mYX40MPvt0g8?bUB^Qw_Wehmbd2|eFob^|(8hK^u!``$_Mmol#S_P|sa6$qB?VAhOhJ<&9F4aBvcu}(kzQ|szUo#*-B@<&~#)it;ym)O98 zjzzjX5pPG|3z?(Ox^;fUf@f65>zv}PpIkq|4RFoK_fl!?{?KeWRV3hh4a3CIF*oh< z;mZ&Alpe8!_c<`gzCLG`>aavjcA%3}m+E<7Mp{;qh@8^E1T{!JX`|`Q#l$W$URc+H z)qgWii)RHmWZjr!I>uiOiGJ0)pniFu-an>@$H}1_Q`uELZ<(UkfUfgdkNjuKwU>Is z52YAAK6Nt%2Exony5-Q3YY7||oV z-WN~w)YxWz`{Sg(y76F^*P$_1(XDLsv;j=8w@y`Z8xgNBQ}BAoZS<4-Vb`QNvv{m# z>A}sA-(=Pgi$g4g!mkmKd4mQWyLIE5MW+;MF)i=Bc-{h!jl9|KEhhHsfep6Wmt~O& z8PtBJevx_uFxC^zo0^9!=ZD`)BeW!Fabo!>OEX|N6eNNzKWcCbc@_P0V9X~jAo3IX|lR@OMG=e)(O$KHrGon6dZVbL-tA2VQK57;6nu_V^))q zevU9IQ|)3aZaTrBG!I`FAo)d{Lc^b7^!}2tooNL=CSYUNCIe1 zM;*l>hMRndz!oZ*Mzr*V^X71jpiYD$<*j==$Ti|oDPSAM&a!f%SCHPV+ zQy!(T2W6&E8Hdhn0}E8mDYV5GMLHuYJR=DLyV#=e=dTcO zIYx^mwb@ZKRpc8F*_aSVl}>voV623Ja%Ja4KolR1nyJK2<@ix$OK~%qi;Fm%Gm4o$ zN)ZTUXF&|~3e`*$wWtYo`(A=s^3%lHNqo>^@DwFnBRjB>BDlxBXo><(9^Xu!9Bvxz zWZ?ou5Y1wS+a-YlH-;c8RZQ6yRV^BIDv9ed4b7?DM6Hk_X%cm-hZ1fC6ZhHuvIWEC zBgTOZrW_V#NCoCpA{NgA=iB^CS6J+32$xRA-r^zxIuQdoQ2QKkdQC_}nn`-5n0nlB zfqIm{8A@O>kr@EC6kVP&rBN<~CcXw>RX@-f(U>`5BX;96qiXQ{Qy)dT{`idL!Do31d;m+H^6&5p88rDH&X80K6Uy zUA*cnL)uVB!QDj7+0kFLM-@liYZnA&gOCT8LbqO0Bv`=lp{mqv*Q4nTS0DbWX#I8e z@SF4bw-exZbze#{I%DHIdLRg(!Topara!Nm+?15Trck5Yb^*eJ?co~*887*nc!YMb zrN`hS##EG)f=lt~9I(+N9vgMY8Vv!sZ;@ZSb(IU>759yW`&h8W#CRK(Dpn#z21^19JB$MzoaKtWkmdvtm zO>y&eP|UOv&ott>)sh^L@VFyzUq7+W^su=NEcW(t^we;2v*0zKaLtTx@g4BhOmf3l zaBKWvPyWrrkG$y4DbS+i6_$+pJ{`@p79Efbrk#NO&p;j=h}h#o465(0HYa`W2f#D= z;%Y(c07u;9WDV*hiJN)~dBbd(WTD7ZSrk?YM~4SQY?{)e*%~F@{X!wBx7W1G!rKSk zLhJ+WF~0|3W6&|d&5>{Yy^O`iTSux}d~sDpozkJy0>$S*r-ZW*#q7ZbY{F)%m;=eU zO7sj`gtE{ewtDJ~xR^mnqrv!l|BCJ*4Yr|X?$=tPQqBDI5$IM`$yv4uHWALW<}Ox6 zb6nOgx7KgPKJYP>47(x)JcJ+G^V<9?@2p&E>5a&$-OyB|zM$ zgOB|4|6%Pd|Dx{ywcR0x?ijkeyCh`jh5?4|?(R@Pxy~peN z)mrP`dp}tFKj6W<_`W{x^Ei*wrrj`Na63;AS@!8+vzPk@G=c{g}`@p>3ANEaa z<^mg|gFR3|LUR1G1Q5miwiE9vo1Ey|cd+$KcLPy(5*0jbuuD-??uS7t_!olfl^8Z4vQFiihYKmlR|*V%*U#sNflJpSi46&_yd0>h~R#$+jc zVv8B%z~~(cqVlv+n`~{=I*)w$jY`-eiYlS(LYgy${35|0jw2>iS2-nZVo0@mEbE*4 z64H!}r*<4-Y#oc#0aQ8Nmc!_eYSLU+CyE2-KU9+iAWSy%wG0kMVSSKlx^Ab{J$pw+K7^vuKIWivz`d+(ztI7OUU5L0;bX+?pw9TYkvpP zch*_2tTPX@BAv4iAIr7g?srn)>M@4Vv6kjGbHlQRKLEvuM{RTD2{HXktRcTMQ7tLr zl;LJFM{s#7iKBT~JLp@ezD@nG;1bk1l5x}K9GUR|MGEsR&Migv>}~JGUqJ%dacNl4uy~}CJSQ}#ojAuF z+KMytbiC%hWeZ*M8dEzRx}d;%GEXVibQ1IIMV-{c!MLrUMBlfs^16^m<>iWzkysP+ zQ>jQ3BQ&bJ^U5w;Z468)r|R*5EgF4r09s{vIb8(BO-=fK`XV=U(<=mPqvZLTnMQ^G zJ4ld04b|X8l3b%e*hWpqpv6#b1BY>r6!sFop|+17Fu<8;Fu;>qR!vCJrW#ZfM71Qr zC~A2TwSbbTe5~z;P48402e_xQJk@KN?_e5p=b#u;qC#uz5KD!`Zn} zPeMvz=1wqg5XPbdBST4=VHSjzI|}|bify4{e4T=BKRkssUR;%>jd=u$R?Ic+u1%03 zSxs2npd&M3P<7x^2i@q^{e~E%$YS&XWYn0U%x|U)WzF8n^aov$!_K}EA0+`X(RAtT zvhJntbdqE2uR6T3w0V`IG*W|Mf+)6rP3~>*Ur5OW;eVNVLnsUbCj_jK^p(Se=&T&ab#juh-%fEy=|~Ua(KE zuhZL>Ru;Vd!;~w@f!O@LILpN)SitNK)>gIYi+y^^_y&#%{tbeRZ4Gwx3pBTz)mA%v{#V~$`P;Km8W$Q0jYwjRY7^m**9`_SkL}hJj0D*67MaOlz~%Fg`i;M_6m!G?iyHO=?1&Kz>f^!*0Pt=do$2{4AplqB znu9cL=!>4!t{)I~Ah^K?G*qoX^Y@FU?iQv0=4rJ9fMpSHCh*OQY*lo+52p^4USR?) zie2nuZlsk@siUr>eZZ!z^TclKr*%`{L=WxfRON;vniO6obnFd6vdM`=D^O_Z1;%m6&6T&lOb5bQ zDrJXFoM5r8Q=rkO@y0k9uvJ#{ydVMq;#h7Y(NDU7%5juBY>3&7*P)-qNBDx;vGvru0C|$D*({Jb_DolOT0q9f7UNuoo70n&*#@S_B1~@ zgL6;XqQ}>FlH9H&=vwY-Wp)Cs`rMq`2ZR7iINjsF&?Ug_k!#b0Qcki_mzU(eDH$M3 zX`5?~C*lZXz^6|Isc1hp1V^bec#HKcFnh-thj7!8X`%&?3Wo`Vjs=fC3FQL-o=cCy zU-pIq!?j0b2OnIPcFE*p&i1~q|B$yoLq>$ghAOcIhIA7R0UHDkJ61*;*2zjDv{+U| z(PM|XAr))>()Pr$V(h26Zfm8|uZcg5eF-KI{KQtSoIfot)Ps zgyoLgH0pJwWeZ7i)v{^2qD6CBr;YZ(g#OV{qQ$iJq(MBY3!izeEjdCr<)|cpBqI7k zMg~TW%<;tYNdOHHJa~2vdSYzNX5>uCp^c|9>U0C_Phs-dX7oyY!DGiiP3!MoecAeP zl=j5_CPA$ViL>G>>H7uC-z*Mjc=RVAdJcnSXJbGzCcq6BQ0}pUCdlAyS2Csd3r%x% zCWP{^t{iT$TxxdhaB!W*W!>WR(`+`Tw1);wDJi!`cU7WegxRNo0pP_E61O#yn>|v4 zGt#9685|a}BmhW#1JZCI3-v+Sb;1ahBY_(w3w1^b(Zmc{M+pHSYb25-yQ4MKqFq`7 zcltk#m^sGLS8{8Ra;peYf5fOALUdEo;2r{k8-ZcgFzX~bjss`_0Xc4;B)NeM&IwJ* z2~F|_f8Z4foD-IX6E@h1G;Ev<4jxDihcnQK#GQ^D;st_GlIMp<6hi z>|ha@M-zQRBQk$Q9NIv(JAubtf|q=N+b+hi9|i0t1qZXkZJWxTJYb;Y4*ZG(cc_?$gBYgm zj$357+8(jFf1FyRfcw?2G2%mva#SKHQvVqwRBlWFh1q6!Z|1JZ>Thl%^p>~*OKw0o zO=G~dDIBjkqlYzIf>lW}C0TixorhTZJabtCd)dyWBS##9P2W1w=(;SYFk=f2UM?uL zu%3HO@c7}EwVkCLzmH}sn@1;GQ!iUlKB3tOZxIp*E<uP;VjHpPZUska6l`eRM;$E(4~=|=JijVXJbsUbflmIAkZI?}VKIU^vYmO-5n)~u z|G*A!X&%32=~MD1SwoAX`k|owH3i+WTFaT*&6PUc67hf>_9+to_BK&b9r>Oaxp_bY zwI2~&A9v9xA@w5Hp%ucw-Q#MWCg}{g~&Dll_p@lh?u|s7g15W zMp1xRE1?GLr?Tu|vV*R-heB>{gHzbkCy)R!U)5N~H4rf4PUlgB3R*x#LEaLdyrs1r zXCwp;OuWDL;vY#g#*?}ylWW~ke0-9I&R7k=xw+^I-poIX-vAB zl7oNU$vWJnu~6t>X+~sc#5kEb%}5Vs+BM$TGiKNqA!d2NLl;i7QG1v|C1iJ&l8c?9 zhdU_2N(PeGxeP5?MN6_fcND{JjQ#oe$JLfTDfWWt_CA?r{8$s}N|ZI(oG~I~XD=0T z5EQ-g9KA{$)5;uI+LaP)l`d(RGO|ruI+bT45U>x|7LyQ~Qk{x-In@tApi^w0OVX8F zB##zBIz%3zs!8iU1>6KWxC3@$anmITIZ*L>gT$TMisW-cx^ju_a|*(-^|)vA@Okjj zd8W_>uoLc6g!T*GW08l>hr+FSqo8{GLcDA>_!aOd-l2HIt(&xyz9yoi@TEi2W%6j} z{N*0%=Nm(GknOa5C2^I=K6hl=`upVt(CNM`z(eqt)cCKmmt;N^3N56!EmY)A_2k8s z*wQvQPZ-9sz!S1qIg^7$W9yUHmR}eH;pFw)R2LOP5tk=1W+c=@K7#Yww{)vM8I zjaXA>aZQ+63j0v-9Cmn!h_o4Z`lv%d`%K#5h120jNE{?6cf&)>Oeq|F#UL8}kP;)D z9^;o4(~%eRP!ua%7VB3T!-XFmx)=5a$5q-oY5)=yine13GjVy5b_~R=g}4fb-wHQI zuT>{J>?8^wB>El4_|->ews1dmaO?HP6AyC}#U+a%N<6sw!*4~JhdW_`1n;xi^;rDx z5CVj?QvI11maqdc`ONFThpl#{ftaL-hFN_lEV*1$YiU>M5LYcSuIcFo!CS9gann0Z zSdwTdwf7Y|Ro{ttzw`H7%)q;O*ZS6}kP-WbBSCt&NJ=c#P2EVaGfxrxS;Wr2ibwfIxp-d$iT#J1OzwQhMldUb$%AZby2AB$dcK z<5hvkZGrz|LFdncC%D2Fzyevx^sJq{#|_FDywdVQk;S->A=4Ft++D z-3v9!l25g(0e7ymulBloJM)OdjDd#lW^^-_pFg*A4FAVs*R$t`|B(yhs1O2SF5R2! z7c%A}sNGo8>`p_-{QMUbv)zMNU8o9aZ_z4=dy+AtH^d2LqfZS9M;7AENk*<}tjXlu zWmNHGlw<3%QDPe4kQI{AUwv+O9k~3|0IP3gkuJvMv1B$?`m$x=D^^sG4&p82l54zAPkS`eP~tbrXw zwf}WR`MV91+on0i5MF0IS8FU9^T^$J&A7Rh9OC7?XCjmczx`v@BVJ^-*aI!1{XPUUzQ`!!BvqXlOcJ25pyOG z-2|>o3O|J06u0-5)YNz8@%5KA3~LJdlwBy*pJ>)?C-p^I*@S90r}u7>es5g%!$d(s z=Gt*!QieXNddk8S%*ioZEBrG(+23CO-E8y3W7ba6ip?u^zB&^f>rzUcmjniwgs5`_ z`6?&YY?QC_kNo4ia}Mi#Tyv-OKL-|kWdKFG+qKO0{=K|0)k&hBoT2v^n3%b?UJ#%C z(Ajgl8)e7^eMxBXR@XuF{O5;DeCsi8UA5L_A8D7T9PG8ah{P;$mh^Wv%o(1rg^opC zgqc6N@Z1drw(P=9GR4^2EsYi~j4LMZ?MxF08Z@BvUGI-i+Z3a~&HKDB!CTaRv`TNs zs@PvgDA*t0`HPJ~_JeO47|CBvaE;PWxwPAVILo0tJ+TS`{vI}Xj?kT+VQ)CtaQ>N& z3cjzGk-T_JD5x0v@tbPyhUay`>htd}L(do*@bE7)fpBIr$e}oRFv|Zof*A(<>(Dy1 zm)z&irhbLmTn)~3OTzl92*;vxwRdHDyw7IMx0uNyKL$|r5zWZCUc26rNZQ$BnmY+a z;dF2hFf##7bBTj5o$tVVy(pOMiZKvQMwzu->o43@L1D{R==c4+_|;ov*iNo>D<@l% z0o5!HEhW9gGbDS``Cdob-|}pA1TAaVKcSiwD}tzB<(>`X)Akl>Nu$?AS$p}*hi>iio*d`o{<+AC{oxwN#Cn6 z9`nam6e9JvY}oRk^PU)M%|8SsKUCV1o9P&g{hbTfwF#pVkvaI?irRXQC@arGL$H&!y;E&d8jjI(`H#xjyPx8 z+F6UzPj2u)rTwUNr9kn?3d@43$ee*Z*dU3}L!5o>p@n?%<9Ngd4AGn9=KX9wt)Ike zV>GS)r#*AzZZ^TN$3Wl2P;Xb>Z)=F0SA*q0hsZgY8ixg;6sz#3?+8qAmrxBunVA@R z4o+Y(0w6xDF8JqW>JQx8Mop|z8aQKN<%y?Ec9m&6H@2dysuIXzRjOG?!~Q+(ySp)~Wn?pp2)Gb!JopNu)g4cF_^nB_&^2xJjR!^qGj2q1rphWfgf-mE4$)2n4@jfH z9a(EdE}d+N8j=Y@OztBLBSZy;mV#s8glUJY!HD2fNZg2ArBa{#$DvYREc4fK;II7@ zbe~j&ik?J}P~HKT==o5ul9k7j%P{ zwf5>_t>f@@i1A^t3AdH8y{MNyD3sdEUx`! z3jgE#ovmc^(SP{RTQpzY_Tv)}NzJVoVpoMG4WBMmu4Qr2p60!D9Xwlwbp*BEywBIB zfUJ7!@s#)4IDYV4J;VljvfOcf$j#SDBV`3#bX(FIoxh+MK)X4ub=wl1h*YI zc5k{08LI5O5vM4b4A;afTt}ig?2dVi4F8n2|ncWFg3$codQ=%o_)fo{fq2hFpU) z#>Eamj#|9Q%Nw(^CV3dqM7=--tg{2tjJ;uQqf(>>XwqZMk5^EPU^v zpr8Gxg#0Tww;UYqxFYYKeGD5V$gVRU=`cD>mhG(br-Y{LD|{(c?uzTi?GLjuPS0tk z1`gw9yRq7K-Wh(P25cMF$fdm0H4=C`l)Aqz0ZBVhYNe?Rn zGmel3Uw?z%O;V=_X!<5&%-VGZ9UJyt^;{T!e(byW^c$}KQhC%sj(QAz@#auPi8{KM zrOzu=_?#_ryS=CT^>q8@7oJ&9qYLodjnTc}c83dF;(p1wUJSL!VG5nI3$YF@Ks50l zd9z2IZZg<+y&J>NPu?_o#O?4)vV*^zYo^8H$VH(+fUIqWX29@8BV6imREKQfFjv3i zvxeT2c+k*?`4>N>3)Vz}=aY6Gewh{=KmGdh;p+Lf>6gb}zvq4dB#D85ZF*qHJSC#! zyKU0JuhJrgZXA*APsD>B>RO>?kCF3jIg$WH44Nji5CAi_)HDft5gY^X@-w;Anj}JU zJV)R5{ttv`OLQ6;nsDwAOl@H*l%13-6r5)=$O8a{7I-dRhK2_Cs)v)Lp@S;9g4qwC z#ezRO5*x_Ajw~4`b0HrQ1Fwi7OJ!@lLG;G<4U$NQ^^|vdsgW$ADW%_dnhw!yvdKPC z%Hem=)^O4ZS*OuUp^cF=VM#TFX}pN|>Mv?p;f78Sf@7KNpBTptW=~~3#T^b}+&i<2 z!lPd6i~bopvg zTB&5Ez1(Dra%Ufoz^p8tK^7I{87>=785C1Msid_-%q-&PHBv!M;itmq^uo``d9h)s z&LRzH%ZqTLln_6{LHvfmlH5OuPEBD_ zM4JB~kF3Zm?Ws^}BSQUz0$dR}@sZ@chBbx8mUm*u+a&jTc`I~tDoaHN)H?*FyzyVv zlZgV&?#Fs#3Dy*?xn4RcV+IpAtF=Q?9aQeA+|`Ahb%T>Vxa~D8I4Jq}gwKj=xD;le zFJp1;$ffi$!>CRz_pvi1#`Vem#8c@_D<;$WIRZAP2YJmaujUQNWbMuv%34HRa+4NG<1^V}1rY)qwV)mZi$W>{fB;re(0zKMf&a>aFyJP$8fwLWpuhzH z#EZo3T^J!nxRCD0wkEzT)7f3WDl50P9=`But%e}iyZ1;E^>oz`Lf4wYPYf8qZNl9g>qsu5YFi~>n=8`)U8uI)44LUuN`JF~MtEMMH z^XkNcvFMcIbZcD8zePmxXT8wMF|-mOWlrklD%0_n^8zFH>QLM;ckJg=3^ zG==$n2uY%3Eg-@zast>%<5nK}V&%I3;~wx%_5G5A2eTF`yb}O}a7NyXQyeWql05!k zPh<-aA({cRKl%Tlc4bQ_AF?c8IyX50AdSuniv66(!9X|E7HP<)#lXXi4yWmd3Q`i! zMGr$x3+Vw!F=Om#5b6Ln)i~6gtpgQV3>rO2u9R8G2YnVFkGGH|;BLFU={T5MHF$#b> z3pv%uCY2V&n7jh9KOa^!#4YuevwhrDhk+N@Chu7`UxO&Gf-Ew|Q~8tMW$!;{0I-=@ zJi6O=$ zYu;=X}|lINsf3{AYm`_{9kqq z4j$vBBP^(HzPGWiUXDQ4jKRu@xCf`M{eNCtitM7#A+%=#Kt1TCd+U_NTJ@T$Tk*cNK-AeF&^jt|-^V3N?$rGP>Fzd@H1Ig< zL*^#huIW257W<*}XGQ49hY=!j^=tHFVU_Ff5A4dG*wrVB|8WoaixdAZ^~^tANJ6thWrt3f%4B{L7bHt|fa!S~JMYB}K*DX?(e=T@HZ|f}8#Y$sCVeUX#!4xCMX=*WW|aF{AQr z5w{+9QwXVJkyvUJ2Y*0GU=jh&m^bvzKt(^Fre-*?w3|mFTq)~LbO8z8E7@t79pg}D zB1?k-ZW+rr9-(EVgcwy!Niy$3rP=Pr?pU%7g!L`XiIrK^}gcx7teD zQVg9ogQ{NETTK_uu?;eI%SjDY)Mw2cF;G}SOHj4>dDY~@wKTJF7Hi6IedGu(skZ8e z_%V2%NK6ui=5eO!%{{ z^FGW?IR?z21=FXzmvU1TG3R7r+0w2WbSPLnVq}!LWi5kGQ`dp0?H=RQmckDvQ9B57 zF^>-v-{{1|H)iw8#r73VPoNiN<5hhXKnzRPZ%*C0E0}!ZHBbyELIsiMDr4BO;2(ys zm2sG#szx^Q-M!Jbi?Cq7&TRzQyxuTWwLfHEgrb!@@*bK^Cr=01EL@p@puyBgr(xe8IY`BiMkrPX66Ryg5Q? z_B|?nuIGCswP`ZZ1|Qr(Vs+$Xk!?jqQ7EgkNgx3Id(?zAoHKJ49{b_vO~d4~5$X3J zKIQ`%=5&0H5@D-7NL<3V=-x4@ZmgbAWAWT^{Kwq1Sf?eL`YW3;6+4d zJ)zq&DVp9SY*t-`$`?Wvzy71T?H~2bt)8)q%_X+BHYe4TFP*oT&HaP*h2Q^?T<7wBse&g0NUtX_AU89EUxaFh>T zQqKILDmLk&RGW6bz0b<#u_@hkmH*^Sb;}>R3{?9(?iflNsLasGo_G)wTIPcqXyiEB z+t4O(%3qw8PAr}Asy-A*60S1LlD*Kt?22&o6S?TdBxzB_5h`6?Ri}gdrQ-zaP(`%v zG?%O!=!$uDc3E9QP&ReMHA^oU5L`1j!TY2rXrzY5Bhs@(aR64jZSo~q`>1Wt#B=1t|b)bRt0G8V-!r!;0A6Y6)A z@Zdp z;V-A+XmT@%(d^shYtA()(1sV3+&52*U+0mmLc$q!=j!InYjH{`=Qt}!t_VmPiHoaP zNno5fO**WeiPy-1cg2IxBY^KNg5M~Pe+4GsktJ|fBxqD6xPlP!KndOT2^)Xy z?l7$l#yz}y0BMs9c%RbsuX-i}FO=Pl&vFVq|?^gUFVFGAQeMz}da_7 zU%H5AmPm7+$oHZbd}ShUodrMIFe(5A;rgUJ@?G(D$Rn*;rA9*$c|(=(S$EFZc-yGl zVSY*;7*)}cvQ4g`_pzaoEaie@-swWa?IQJX*%eNLA4XN6O15B1eqB6s*=YTEe!I}c zP;beD`Gv=mMyi=g>N_2^7UP#xj0Y!CFi?O`vzssUk#FFpF!=&);Kcf^f{P6AJa|=( zPlEyj(MH)@^ed`7<7Ls(AF4wBCRBkxLcuFWp(R1#CPk4yUC}E`u_aIOrbvmuOv$TK zsijtlz+0ZLuU*cnUu&2`+k(OwC|15-#7{)xfGX}@z&1N2L$IZKC{6nH7&v8?#NCRd zc1m9D%5gRFaz^{&(qD}bsNs#O(TXX-I}S~q-mF1>NpMAlfzPAO{{Ooj5M=hNRMWJj z%knz+M}zc9k*)Yy%H*VWJ=`b=D`hTq5 zbS^~Z-L-Ps((QB?NH;iq)gV?q_hN6Q<*r8jWG=Sku=J@y;&^V-n)j_@iHq;wcEIib z$V0W3O$A8i+`YV>jM;2!4o~%Oz84?)W>OYeu}>; z52JMgfKxHTOWMx59m?8ZrP9^bT0<=~%J-$E3oXv*5_8Qp$6q+6Qul5VO2K@*!R%|Gpc{6zz^{$uJb z&TMku$T$aPFxhanJ1%-I$R@MIvC)_A#OL1ZKWTYSd6?;@F9(~hfiNTfP~|W?AmObj ziWL8gwmR+PfjLfT`>Af|(1f-$?r>vmFR2l3!jS8xF2{uD$W@mjc^PEGCcFIjPXfpU zO90n}CQTJtw8N9%FEm*iR8^fRHKXRw7I#YRWI7{9?Gh(4#QfjiKb&&jIgg5c?&M>^ zNWM9TBrhODV5sBS-a*jScXl4i)2eC7LAMmC zYHh|Fny!K7IP1*x!?eu=#YswJtP>!4q+P%@r7hYE`KjSdEC{0N zn?FUwHaOt8@QdPN>DLp4zX(=uX_lN~Qq->H6#k~lQ#I9;6!ke6_J{vGLDc@$ll3!- zvT%Z(#;#eV*L)J;viaoJ?pUQ;Y@&Ca=*qN9Quo&EQJb9b<(xWMHJR6vgrkj+y+(DDDcST=D4e)-{P{`#!}@W6 zZJ5{_fI<@>#N?in+uY)jKvOErv>95cjR`kmJ~*e!3L?`e8fDGwX~Raa7EA8blZKD| zL|E;rOMvrps7hS^Q{@!pp5RMKNu#P!rx1q3vgHr*CZ~#i*Z5dX@Q9Bea3>tA5-+By zqq6}j)Zutx7W7u&6OljfpFeg${B{(t4F4s^D;Lg=uOEmZ_;2G=n(E}TyGc)O&1-Nw z7dY2;g&-#{84)%e`-NHg?E8OH!T4p=t9l~`2Vf4qh1BZCPt#LiXoC*cpq?S7`Kj!` z?SK(6RJ1+3Z_U~mqR3ZXzv;I_xdfog2)7c_!d)WnmLn1G8pNIVNh!K`H zlHfE+q}-wi1vhix7tmOTf)Fu^z3lXqU*K>6rhH1)JTP!3(T(jmFpB-W(!va2?m0J% zK^bUF$c&Ioyd3FHKXnwG1;_AD{+!}8n*?_Y>Us?T>MmmtIHgR8D|xYyzOT(Bxr1qY zfU~wJlM^-iPFa3!yZhq;RoM14FVz4??)P%{_!>S_z8v3v#FIJ%H_h;e z?~&Ek`#r104`JWd1MA<^^`-Cj?CB$bi&*EwznVK}oYczDSmRS&_S{I5^45Mz6cizN z%?SDG_Zf?Ut^&Mom@pnGg!qdZfaCRg%arYzYHNaP81><3sA z?)jA^!Etm6K2(xe`uSkPod`v2l{W7#Ib$W9+0@`n;oehAZ-CeqZ5Q0jvaTQ^6 z#-*eSmnz!S07_Y7mR!1DnC@mqEjGxe)? z)=1%rO+zxxsJrab979oDpetU9FEmQ0o#%wiZ>9vTJFutxs$Qaq*|=e|Y>7NBaO`ym zB@$t9W@3XVA3O}kZjkB-z6x}UW0`SiQ=?^wjhyduZZTrS`1xz8pi8w3c z^Hmk?sf>>92_83@W`~N4ZZ*C|nCp_S74`^9_n;$btsQs+vaKKkAz_PId1a`EKu z#m~BS&*rVO`yL5ws@GGy-~B~5EtxQS`&lm(wU0^gzi9!KOjPL;RJOMz79+rZP>heA zWue=VX}`j@#|5$8Y!BVxh3_cnfFgFHZAc?`#(gNGc2Oc}qjwcD7-M#$%UNT0$J;2x z)p&c7NKD-T)=X;S3YYvSw<><=oX0~I*A%xrLG zUK7f(fW+6#;Gzq)-W`W%kENOwuqZxhH}aVw{Cp}xB_#pn!yi)rmY5kBxW|z*Kdd4t ze8C!8I7}KoqcO+O7aOYOL+D1S{IT-!)$$APU~S!OIAXC1QKbB0;(%S$<}-A;#g;Q! zxc6Yvk_fc6Ih)Q#r%#5?!Y(Pp>2wOIh*^|csl%PzDsN>rmV3?wSno8KhTxy9w;5Ez zxYQ6jwz;dU-5Nz_SjB9FSaPrs=$-bS?}gCa`)x=vBK9-iCq4B8NMhS|vj=tXX(P=Y zRyWm|rs=|NroLm0@lJnNSrwf59)I+rm&2#5%CWZFA4}v$Zn?cOgRabqs&geQ8 zHzeUV4Ybrv0^m6=WUSX_%u{9mAAeKq*ndwS)M3ak8G zYRQ`-b}kOh>!)C3#>P`L&{%Cn%Xy;q@ONcQr4GVxFFiKxr7{Mz&m zsRXx;=rbJ877NyAQiW;>4(?t4!?qQJeQ4~JCi{q8D~NV|({T*9f$VTt$dJy4K{~B= zfX2d@;GxW&8USYd8cIP|b-xfNNvE`_S#ia*s?nJ zOyVtg2mHPFamJ_8Z0Q6KYmf)T+PMK!wImRxu8C&XnsLQtvi$MQDHS$Wlpy&Kd zd#&@;xMjlg1Cy+p@4IO7tiSEeo8Crs3z34Yj2YU#MNC1RRnS9-V^uqOByGN3&kW5@ zz7AE$1+H)Y_<6UjLd553fo2IU>i+B$NRcCi&+TvC)f;Twx96FA9739~(){7z#F^dX zgKaxZ)*kc<>TSAAa_{!bwFpk#AfAJJqlP50RIMXODYS+>!FQ$#K$wCMOP%6R4h0z5pnvKg^ z`I;De{~9*3=Ak@F+0x3>9Q7mPD}CL1m|wPVGUhj4SiU%O?S7h zI9k6jTt)UFFP_|#Cn(kF0WbNw27PzaqFI^@D!)`~JwY zaQEEe_BqUu()HfyH3>Yv`ugu-qZw{}iXSP+W7%DVP_U9 z)rBNVh^0vKqc=?L1hspd1-RYZN6+fL-Etqf1SQjYmHZD`J4{=oQB&N@GlKrP;m)Ss zT2yce3u21k9;5)i=3JfrOcWPrlJEqQ(M$My*ogfOZ7rA9B4j95{dO6f(2d_>$ev7P z*u5hSE%fc>875D>{-_@L>5>s)f`)~_K;|d1Ju_S1GvV}stFDT4dX*%|xrF@MPpW;y zfB;A9W^wm#m^h?})-%1AiO^MS!jFRM~_f_a{o6SiRqq^=z?ek~D`C_#64{Z_ENZJ+N+nE=rJo4g4 z(M|8*FD=yKCDGI{$$XX7g)vHqeF>GWxxR{TY9Yp5mEl%HZR#L8?|4k$nBRP4#F5YT{wJPFsEG6` zX@5P1po(j8&=^a@nk$uHlv*N&1DDa^v9|5ca||P3q&0>J#2FYXs7)V!n*#QsH$?xz z@luYznk4N(y9s;os;VO zmQQRo@ll#0SF^%F5iD)AEkS#{ zs)=C-HijQ_-}QG|F7?fla44u#>@-o1|*{$v-QliL)=BR zx+MSfyhr>(tN7q7_d0! zm3XAnx_Mu4Orb~Q^WNfmRW#oa69gOo_>c3R?YBgW-?<}SdAn3zl_1x2opN*=M|>pf zS-Lde`X!W|c^mtsjKA(j zlM@a3Cc~)y`zk8<7{i9SxP$s!I9jm1zFf?fX~%SPuO%27p_s#nj^r%dhw3iDeru%TK{1f`Y+Zw8~LKtlI z+|NRflrC8b+t7H=d-_2@#ghZ~@AF`3?)ct`nwEV;50`ZNvJG8!1NGUH_4T@2d z{n?OH@N#hEWE=7f1oNr|fGb@={&+$RGgc<=YER$EqU(}0^r{UWv($iT&GlpJ zRu^2}4TIn|TWvRQJihriwhUvCK*Y52-u~C?Iy;Ew3~NtLVi}Bw*}mkp5?ufMo}7fr zLekum?N1{=&!~_)+b@$Y$rpWZFEEUG>^wV~{iMkNOnpyCe?VSB>^UwDrxpa=(xcQlNx#uWl-;JmjacxWNEQl7ybU=V9 z`*}_jZbyZPKRw?Y(_Ve_rhp8rm{q||I%b=YpUpnzJIn=*D0^(Tk91%md5V3D#HQF` z2Z1uU7crt@6xM3q9_HB1Bxr#Gt2 zn2i(YV@5n;CBq#;xHl3zL!ML#%iZ|`N{Y)OG;Vd6Lox~pIJ+{kFVATVQ`?E_lBIg= z8#sP`STy;)jR6W@_@lv7I_d|2@i?Ow5+TWyT1rq9+8}c*lyY)-6gLp<&Ho|pt)k-G zx;E{?-QC^YodkDx4IT>D5Ii`AyGw9)50K#Q?!nzP1W5iOd%yN|kCyTE82@>l*0a{S z=QXcey}DR^2=ws*U)(f_JkAhZ{G%95cbZC@c;M3x;&(dp6p@m`3JO?ps~`$t$&|F8 zx$h1aWAdC7k(}jU3NHVuD4?GKP=9Xu6uh@Y3%>|A%U*;O_43F)T}V)fKkjG#OBw-0 z9B2_}<-vNIFB{UyIafwP(+b|0bXq`0_UZy(rYnvN3tcu!iK-4brkQI%XdW6|#=Qw34>{>2 zhJxo;gfJ6Y4@w}4^9z#Z`v{g+w`5XeaCaTWaY;s)Aoo=9p)pcX>Oj@T3>&{oG-YQ` z^ZET(QJ$T3o_Ms{R5fnT)iAP*Ay;VC05MvtI~oqLs3QkNXCHmC#IB9&&tv}4w&kgWu1vZ}_< z<1}<)`kRz01Of8eCP$LePp0z2fGh zOT8Kv!_mi0;tC+sGNz||?ovF~{g@&`fOgoh*`{Ei3@m*;1i`X(B+|u+Iwo7j`u5%coOD)B zojCT82|KFTaKvVYsci&UKOMTQ#=09I@IG)8)GCck@$2=s-$~5wpL~qBubeX*R94_WnZo z0Q$CFXI%70jNbYdCs= z45H&#qi@uLp$L%A)1a*R02BrQWE+fTJ=%@lsFODsUwf>|2C1twZD^E_OP6T-uK*x- z`@Pt76*WO;j5l%|X5=O39Pd{H6b!7E%UbP3sjwpYcQ{&4{UoRohW9wz?7o_FWsDzi zwH+PrNXVOTa#x86IjZl@f4Qq6@m@9ix%~CMX1x8t<#xUOp+LNi{(}W$T=d>H$J~&t zaQdUq#(SZHsiT{povbYyH7lnNLL}$k{o6OMesp{#dy_c5f9ZmprTze4@r_1YF<-|Z$fg$7Qq)eL}Gp{6pNwa))zuIpF8t3UWDFOOB%+K7Zr{IDVymPA|Oh} z7lIh26D@9j0h4ib566x7`Mq?Jib}oIb5k0g$Ehus_UUM8_(vimD2tS}gUiYtvPyuv zcSK_FefV4At60m(63Sjx7((&<5OmT~efHF{3D~|gGMasV7ju=I(i+Lu$-~CL#mo!5 z@C(Q!%Q`VD_36$qNL$JV3Z_!K9p_AIl;+~$)ce5-Mtba4@Zn(%6}5hX*zI{Yq2B+< zpv1^LPK$#fTvW*{^c&FS#Eb^r?PNy=-=kX`+-kj|K~ciJl4xHcKsZ}fBW&|w?5Dj- z^V+k6O3LZ2qw>S}6*$r=VfMN{s9+y;CH}@a?yC5mYm9WU^UCMDrtbp=S<=cxBMX!I zLNiMxlj5w~*&Ee4cSdO_^2pht2jTKLY2`QLqs9_+dpFPLO^ycTRG-arypf0@L;XS5IY@iu{479Qg zW#3V>K~byZ8R%LjlCX0?neN~lghEN-Hfc`uuRhw%}=6aR2|l1g~n!0Bhpl+liDp!9hzC~kAizcv8zab zApU*u*_Jh)yM0CL=D9OZWmcpoc|J|r6OLRlI zY-jj^|Ff#KrXbD#>S>WK=qa3Y^Rr^3pWTBNo*WXIX*IUAbgjzf5u?YpBmO9wll{H$}UeEb~U! zUE#9XdNteD}2vmDlwufD=`s{?+*C8HBw zB6Qvh_nj9}P$FGeCr@uw5?^56PQ|XlCcG`vLuy~MFgwMr!?=r{&%s>Zt85I>7aLgSk`-= zIlEEbiMgv2$r&w>xf;b#Q&zlzoJS7HKga?1&_hAxg341Nwzn*ACt7jfe7Aui#axG6-+L9ol-7__@-x_~=f%NPGT=VyynKp^Y=Eo6)Obn?wPo zUbl5_Fh=pmO=t72w$G7uXJ)}xyJP#J@5T?J@4*p{M>3E_Ys>x9^PYc`1BEG92gqi_ z(pHO2BU#U10a;qa)1%}z$sa0<77Twih2Fuytv}r6SCU|{aSm#6+B<~h0+Rl778t@YN`O=i!-k+ZCgP5}dR098y+&R&hTc>>PbAaR39c;CSUmhc z9}AuiZNViPU(!*8cA2^GVQi$Jum82ObU7B-Cwoq|lcM$8(AGa(v$a8I7okewhIdU#*Hvuve48-CP4f%DL*7k+D8EwvqpNm+R{sg1^= z&33@}KJ9CQYYFqQ2- zW&Llz+SL#4x<<-J&z<+jR%KdtVVz#DZLl^ULZYiiUH;Vut3Lmc+X|;Y%-hVkwZr^9 z@&1rdy>4m~d|PUF`9X{x_+yi#E3BP6 zh*!2TM}1NK_J)^>n-KU|2a{=^D!O8D3F%uil_!)yxe4K{y_V86yL$;}<5)@&SvdmL zxzixz7WXNf05T-I z+Q22LK+K==bn%7zO7(J7{4BGCc47pG6uVawjju~ISkNyb04(mBSBofdc>^;8zVl32 zHuHe`U?#C$JscOc2k9LW^BB9`s^Da?65_|{#wsE8`KFS7GQAR%e!I06Re?`cE%nRL z##wOAU8bm*=wrIsHwtkM^a$==k7Eaq=MK0#pT@Z4(0Gtj2nr+pxNuzDcJEc)7(-3@ zHvwY?3;$L}R-Ol$iZh34A!e0DXy1K6P3hkAK8gvcmwinLt&c%d4oeDSfQD#0zL*KL zy)?lQJ)kxtM@pl1{R)hAkBAAtFchXtxEf))8!yLLrtha$;qTD)2ACbv$uJZGiE-_A zWm_>_c6~e4)rtz}>Du;Ga`c_Oz%hiozbf!FVU&Mag~8F}g!}QrYLfTTh_CdEyJ$VB z3OBjW7fTPlza#&N)On0<>p3zgIC>lmf8yx*< zF7T|L!WcB)&1`P$&6d+KI`5=M4w4+4x%I%z_KNe9Mj(cX6_;oC(~ z)#d)y3QGiPx=4)H=E0>s?7VK4!njSk0gOXZ@k%am(Ui$%I4k;glUec97~Qg-JfTv0 zBlk2|DbBGb`}PY_1Ee9(7QPiGWu>A724yL1s}nr)p#kVN&QpqhoN&8OOzNZIS_D@& z074ijrj_+E5Z(~$#Lhp5d<+k|Pk;toQYN_+eAUZ760Lg46Z|8}(xw^CjRUff_a$1mICySdV4X$PoDR9YAkoX~ejQa3cqj*8lQGMmS@C-`Yb4h3FH-am&7~+=qa4 z=jXWMJGp%K=3txOANf=Aw*l!mJqFe&v2-iJ7X2JLmOs4+EHq^}-A?hi?5H6;vn#Z) zo986z%mBKf{U&AWcw&?&@7~+yxZ8e7mJ$rdUoRPQK-Mb&=l+Xj`c_=jW9rOrY*7*?$%lZVXw5j*V zDnjyW76_Rv51YCo~nH(ffQR=qt2(S=Gl)1R-8(}+vBGmyZX>w`dz@GoS$6N5VnNKYCsOQr@(%pv zzkrPNO3d>U`2e`rrCKGObieqDH}LpQ=qTr++Gh&v9joaCe8W3`QRTZGvSweGMSxEe zj2Jmiajn$H3xhRTBd%hwuES)P+(o#Z3@UzCN-rLFI;%pSQmG;pQP_}zx&c(c?4%-iwjfGz2AhWKSg)A zDjw#EooXf+5|&pUguJvS+%p%PANGsAW&wh4H>o%NN|~uxPq`Iy?GsWx4q}~N3!jD0 zXwXeBcY`vlCVl~K@fp&AGh_5XdI^~g+*1NKtD260yLpa$bQ3!lA^$jZEgwvjj;xqS zO-@)Of@y6}U~spMSYs=?q%u6s)xE8iJw?S`Xx)O|{#6vr{c9Vni5*T*=RW3-G=qK9 z(wN75?-PHviQ#1;))%`O27iz=fFbri_|1=rY!2t?PbHeRNA~YuEuu{3Z97<|jxgk{ z@zw0tZ%q#=vv+x^YUIK=k(W{8$#FO6m%|TVcW;BkI|-G?a~rN&1vGh|yJ=sV<{x9Lg=Sf|O73`Z(UegKdpfA)95pTAqKeoB>yhc{z;4|XwdaN9{B3QCm{of-i zOO1aQPJ~E+g%gtxtHbjOo}vdTE$v@Y6+ARbk}?v@D`cv5Z0Da~Li_(G!ii>9)-j}B z%G=qmycZLSP&Lq3J>of&l@b@&&jX^)lZrv#sJWRE!mPy0;5Uq^NECyEhI7U8rw(sd zjHDB_O6HHZP^g%^EE1(;_KBTm8u@8`jok9M9?mpiAHo z_+=1-p+@N>HB?Vsm3x&{`%ytvTr6yF2?Osr1W&3JAiWf)L%$^)*}3Na`pw{ z?UdX#e;`H}3Oe|kEOs15L?1BDUOU{wgYa37Q97O)fm1l22IOLrP77*+Q=gj`QPZ5) zGSbtYpZ7J@UC_$xYR5zB{?ti;v{n3#$PQwto5YS{yO#rQ7TdQjY=_vV9MuQaZUTKd5b@WD{hY()da*y2iMlBJBV3WN|$nd2lV-_v;OG! zefDuzWy7QBP&ldBrb-w9;kJE}z)ox(nIqi>6R;5of2}oCGtEJ)R3>I9FCU)+UTC0sfJaYb32ur%yK}K*e{vI!MCvl&ThfrOz+-g#-tW~%dzx7%+2Et8; zv$wFTEv;X&4$w69V&LaFBm7{s zxe)3Q_3(Ap*h2bx8D;8Y64BIRh721Y2Oocmp=-ljhLP*FZyaOocTLVlI>~ndrn)Iy znEiFH_lElB0zO+q50v4h4@UmLrTflYyXCd9^{J6{>9b`IIve@o;v3^P@w<=4`Vrna z^22kU3MY73o?dE%u?M8TdL{~gZgg5#ejNT?apL9oe){%BLHbyl*xw7^b2>NXK0M#L z+)=(DX%N=`vfNn(a}m2V%l71ta3T!{a?=7OpucJYFX8^3!eaot6?O>o>+Yh>q&}14 z7>kDRIR!=8|7*wH|0 z*H-f=qL@<|&MxuZD=QV$*j=rOw5U|=T=+b1ihfj4rur7mKqD7X&w2Z4m?+jwP;Dbu zrfl)#gIDL*LDzGQ8_qY9|qwPlIuU3H4)#6z5ld6zE|ai=0^eHh5ELtkbkOdhFTT@#)G2@_zm^s2QB z@1TLx$1bI<+wWu!5I?raSSb#7NX{ja!21WcN$0~cbD1>5Gf{aA?P!p)S2QY=;G6E1 zaOZ#QbjU~iVu`eafnb%M+k+y$l*EBWihL$b7pFa??JKP`ARMe`&Sc`@;xNMaM^ix5 zBzC{E-8=jN=SoN7ma3<~t=4KmEvmu<#axXMEy*&?`D7y_EpnH7`NR$zrcqWA?&NW@OBdk)aO|>4P(UJDj;(dgq}uX4he}5l+~(+cqllIe^(AGT*>oP zd7CXaSGBvW&(@v2A)n)9kxm>&KzXOVfDa4fznO;Asi%NoWY5xZeK!A(WTu)0b7>Hl z$0BFcbN>qwQ{A`_+lOGnF8wH)Dg*;;WEOD=Zc*0!7=mj1OhYAeN#7hrggH+#;Q2=n z5`AXO+=DO;NDpg<$8%4hjCO4LABbpO$3o9T8ZQ{=DT7z&p(1k!9i}G0x2NE5vg!cW zV$$xn*SdvM*btIg!!=#`SLHA<8jPv#`_Gd+ zP8F{+c@YvsJ3JNIf6U}>|2)uY^$k6-NZuPP;vT?Rq`FJuIe^(H*o@T7kT$H<;yzHw zTbEz2I~^#bO$tU=$vZ^G?;G7oWoDp5C}meQGMgQ|!^W>)h+^j=9Ll86^R<82yB_|^ ztvUPSxXU-^pW8Idl5=O>+Zw31EF6JjM7IP!rX)#0?-4|-d$HOe3Z%HOBWC6x>@EpM zI=2dkIjv!BrQO2Eue}*$`U-$ZQ;(+9=MH}Q41lDWO_;u)Gg|shA})ys00h!ffSUru zZ*LLRs)fOUL+I|vXkZ}` zQE;Oz=m#IJrPBl9wn-+;Fc3XY)esAcJS8ds3r%87IgU%&MoDWDKi+j-gC#(jm*;d@ zr}zCkrNX`D)^!r9;G-DVayQW)@s9=#h_NVphDyLQk%K<4!U9Mv(@ zV@j2h6=$g~mm()^*ql|t>4j;7*!_`{%VY_FE`IBTy4A%V6mW>#)8`S*074wt>7|JS z?nW)C$Zvp`9lFz30q_m@4tKR9AEm0CEp-l1z!GLZD?DY35nk zMu&Swgo6(3{(Kp($cTp=+M9O;A0 z>9D*2Mjl%|WqhN(NFSZ~aFO#r^wSj00+WuT#Y|k86IKcuA4gIxh$|K&!n4IFVfnJ zN#m|%x7Zhb{$W!2vQi@)HO23yVo#fVQetmo__O6h(?v}I9L8?8c!>9+5{Z2~Z zzg>A!s#$SSZyaTueo)fFRF}(Gp|s$-OuEF!aDPTq3T}ged>bT z9ZYdqR8%F@lu6`+1)#AW{c=ey#OnLjR{O`S`mcFFC39se`e?Zc0s2u1TBb9?Fh;gB z!wyzs$(*Kdf7q^}&j4JZg~tHs-z6Yqvhb%tWJBuAAgUo3#b_GYt6g_u>5)VyaO89y z;nLLwCTyx0%&t2>LCeh}{cDr30&E$}sSdSUx*=7v)%OGkCEonF2N3Im6O)ZoKrCc6 zXa^On#&h55(@#e1?Ga)RZS1;!?I- zK+&5dBsuftPvsTgvEJZAnX%Yt9{+Ph2_^Vw-pfZFEr&q!P6(kXv_R0p(O+kx!hl*+ zGNkCodK7`9cJ7*TrgNRbl20rCe{s0647=)MP{YAb-?+QGo`{RF>sbjOSds(&*|-@} zJ~ix&(pxMk3hbj@lXj6TF;xE8&tYhQH^7l)lPrTV=SgdcX{<$U>8Y%jy0;t^O-G#& zsKo$=Qyo&=Jv#~-mcMCtPq6>})m6HRNh7SYJ%IKCyEYR5Gf2x25HcPfKkSO)W zNtNn8k}3BqOQu-3KljN&+wg{5Sl7qd)(4m3(SC{8UjD5FV312Is>c&^(q7CN^P2Dp zzEu%-kEOaT23D-qDo(!^q8s-oqq8`$?-z8MvDs!|qtW}HD_r*bjtU))a`%tlFuDEF zY#0pkUuM2Ot5Is5u?y^aC!2q2YhCw|-Q>cgi6yCk=?zd&NfB=Btv~XssS0V1hQN0Q z|3=>P87H9L)h7wbB!7`-)gfJH;Ox_PMpmfcZT<;~hyu_Y0>tyk128m4xiJS-lPQZhUp zn^00yG~VP%s`m;XEy+RzC!g1%Fg~$<$!Cn>i8CAnl|oWC@a=LQ4fT;^X%Vj*fCMs_ zwmZ#1;Ngc-N*48>#?8g@S-mG!`kjEmZVE4Nouhc#o&iL(o1aUc0ET!4pWIPA0+oX0 z!ie+N_(|#ag_F|OFj)jS_&73VGwbJZsekg)_pGEr|Gc6Bwbi}yFB&)BNY!LBg{S2O zWSrK(oQon){D`r|DTw|Uy`TH)4O3rN8jd-|5BfcF52J<;#PzH^1IwgJg%Q^uIM+%Q zQk|)Hw{E~aCxWOukL>FiYpR8h2guWN%4@ZTwp3*f#gHd>CD2=(v6fn>Sn?HN727^R z=|Y4WW_DxO9lpV}>yIu3>3Sz}3WnE@tyn4S}t_ox~kP(^;2TG|D@t*{dSLU`$AYdX!!* z#7+YmHRBbjB-7l7ioLSfqS9cU{UfjnfJ< zC;&Due@i^0cagXW#Rhu40;H>J*@*S-q8bK5xssQ>HI-|VQvM>j5xstq1W&8B*{*+^ zRn;Q+!zd)MLln($Ife`e^AgEJ$HF~&%k|J0+<7}SC+=nqmc1|B`9XFY+0k>QV3+u& zuuNQPBf}?e)@q0@U6i1?BSb~Gr8Vr9GlKy@j@R+1vb)yLT6~CmHuJL(TDTv0run%X z74A;7(?dXps|@bCw~NG$%5z)1FTlux)|$KGUQ^r?dj#*S_Xl$Yqo$PH+4R>0h@#EQ zS<-CI^fP@wN^r`V4Xov*$s=fG@K++=D%g>7JI?FR>l-o!a6!qnK*e&3LhM&?@pNW~ zw%sI#%?dkEo%!CZo4wJBO>-UaoYcDzeZTgsQQb)z!mN)|SE?_P=fe@|v_RCGBIh~1 ze)JWuMII=^;@#Md{Z%smCk}kJidg`^{-~)vOMfTu=2zLgeid)=4FbVoV1&vsy8~}Y zm}!{HI$ckSoFzki>Za|d#C#v)sL-yRs|_B8j323k^*+@4!*)E&LQE|uaCXCfdc6w< zL`juyz8rlHX&;5J(HJD>1N~T+r_HSsvrDM&D?{t=3v53j_9ka1C$DOiFJ2^)sPYa} zZpx*s5mNM~CtG%CV^!S<ee-Cch)&rY zOHnfw88zylWzn)QW6;G}qhk-T*U7WR&R|%*%Fxc9h6+1nHnDsbMc`4fgL1A=m#tEo zIaat-8~7zc5@H$phcGBo7i&+)>K)B!MZeDo zcc*TaSI$m^^;NdoUFhibf>bzuizuDDwYi>*{rqlp@)A~eL>v#Y7L@P+(nfUX^CnAl zQjI-M^87H|*?K17mR0w?DKWf%lDtlSbBKa)|4!xDgoZW;?%NEbo0vh&nc?BjbW<`n ziBsRg*&n|dlwQb6j(;sb?jLX;kdD;OMc0UB@_V4?=-|z+`Kz}vTuzK;yiEPwxEWMq zU2xJ|sDwJrs5l)t)G=Z9a1jZ40fJ(ShI05Yo1Zg#?hnB(1??b7W>ZLaDg*<#?n0M? zsWlWw+w|_0-4d(Uma^8Kl=M}R{~fE!7ps}ee13Rv2XhI6uZ@P^rGjv@+r1?1K*23S zAue)a46Egp>G^+LivE8S4B#}KIC>izd+?6Wk9&9+8$Yy7ySxCk+MG0KPg@WI`vk22 zeZr?bnL4JNM9{GVC6fNnU|`P1T_0Q_Xry9j=Q;)}Jzvsde{PuBVtlvrt+g}PkncOr zdp<-%G2g0R49aRMH3e5`zIoHn=LE zRKbr}@NI((c&h6`qE@!%U2l)xNKHH@j4Ow)J1msRg@Kl|J}|L98%^V+pC8uAX`ghu6&Q2T|T>}`>X zJ~&0Vw)~q0|GPra<{Ly+q#YIv%cTVgytv%RGb=lKy(KbH?JRbFU6d>NFlST~@wDWf zz2U?k3SHz5OrdLp{^P~F@JgX))%Ad1ytptyrnm<%QK>q8IIr74&_1bTR$}a2dZy0M zw##=$f0v>U&A5GDNlthvZW62U04%>pc?#IXJH1UPyikw0 zS49h%{#P&%xr5kAE=~OLc;B;I_l3EGO<|7*k)4@DG==?gPC!HAPEjhVLhFOQc0Vm- zi5|jH?X^K>SUh;Ta=&dSnLYS83IsrL$OFOkjc@b;&`KXta-Emwow8r`g7DM>pHs4> zPBzpt4ia=dZGd&qrnZDM!4@_Lk8A43K>>~#e+2{BBy*GUoLLo2qnvABUN1=C$Yxk` zAwj8?jU~Y42OY=wjV`D7ttrlFAyw z)_M&9dkcK+-Tmc-hei5$Zkvcu+jgA=6TVe&#Nd3+om&b(*pa*F2uFd~1Xl>MBCoLa zm9{7JTq;Bk576QfryB+WT@;Q+*ao@a)^^EVYa=kl(I>iRx0u|_-R^UK;DC`0j!@MJ zLJ3_v1)~U9F?l7JRJBR8sQ~X!g-sh2IzFX1SZop_dFZ_^AFr3&1>T$U}Z9**)MH;4Xy|!7~zL4##IUqT05(IIM0MNRR5~ee#wEEHID&J4{WHa1!|qB-CP2 zxOE4_ev_HNi^V&Ba)M9WMVDYVj8U2p9L+!xJCs78$CQWT_hjDwod3?bEJm3voY(F1 z(Lyq3N{oDjko4MI1r2J1j-afOc$pMKk!E#@`T`5<*4oz(y;i2Q;laUkyVB{*mHzd! zSeQ9XoGFux3^;A00>iU6-u3|(m=8TaR^>!|>ojG*t&|XdkJ|N%M1;hGxEGM#TqN8{ z#*JJ^BTpt_eGLZQ+TbS@7K8^Xvx3BI7+|SS2@`M4RHw_|ygR%LD8xmMRj0*~Gc`w% zIcy>?KwLFH4T-0dD7A{j(B@>4%dbz-AJbzJK3Q6}C)Vuq=A?57T4t7@NhDDb`ssBE zeCoBr;Mf`E5>QisnvA+C%t6(u1VDYN^R=i2B5FsGXI9!uO~<+OE_BiDlpfT>H%UMypS4stF*z0Y1b_Az6G;sDhz*y4JQER`|{LJXVB}G70!<;j@hy|Q2Bw8r^7s>1YuvPmf5I4RC1F1cG?E!1o9)=EV9_#79#%1HA zhEnW#cGJkm;}pETlO5ve#zCixomwM{K!fV~(f;E{zb=!M3%d5Q;9M^KCFPUmKa$tK z&poYEwM;(T`Lh&={eTi*juq(`^=tFJg1eF&o?nG5)Ucjhg z!c)|8d>F~D=SKAWp=V3zsE;eFOy~1fYgJ^{o&YE@XIGFlv;3r;b@}SUf0Z18f-w!n zte^=k#P+i&?R(yXw?ummqPS3cYoEbNr7}%59I5zycanqn0q^XC71V&df$3{GavF6^ z@Ued)IFmz^$Z>8}M-C%DWB%o@t=f)iMNalj*~BLXc9moo_m~dazUXLG&N+;GgBXO` zO@n$HL~yBbeG`O!nH%kCE>PrfMu9CM`y0bv=H3Qbr%l`vbzz?Pb--H@Re zUP*GC%6WTTw|OFA6+>gy*bd4}EFI*@yI~VRt8I0sm!0qzoQ8!Pn*Ixg_IgKyBN>85 z7J_&78BL;|HVJ&9R#|Ww0&PFMd-OULz0bd0JmZIKEN8u8IbY|qKk;k(ek>Hu%D+Tt z8JOAn#;fdag!7>1_a-#NCskC)x4PhSPfYvu16QxDJfdg7V!3mKiqI&}M;0*NVSvh-q^y`#J3 zZv!_yad+g!61G|^61d})smhpy!;EMH1H(f5Mu7$>8o zMb&y7Vvvr&D2y@N;)*hv_mu$KSPJn1YR|h_WdBN0y?<`iD%;Ss;s&9+po`0@AyEW5 zgnlmI2tuAf<mC7q0^ zi#dT4=xTmgpioTQo%taDiOhCpc2ao?hlXVZ1x}iZGPhZ@T3zcUR@yP)c9LWTJAfD_ zvV`W2L6*)fT2s-kP(_6~r4s@(CqFnxJ=s~H--b!*a9p}zjwmKQqfO_T$xmN{;52d6u!4EoSYLl@^TnmVlAO5i!b#^Z@t7Pk#Cdz|NEofEFpLf8>e_<_tGzF3-#5lWc1pF7u6_CZm`S@%54l6z2le?fNIRhmX<=;_ zh#}YK>4DPMwj_{b4`T90Or?g_MqPah)h%E3oE%U_9;>I^{dE%F8Bd7#`o*qT=t>j* zt|Ju~)V&}zU-RW=4o1mT%y-}%?hq+~#tFQ%Lg`+5V~t^y-^82AXenF;yza(svdCXZ zrhhMLTE-ur-_O9no@3zr;mx>S4KFz%mdc1^G#XR7JX|~_F@*b76jQvMOeORE7*v)M zlPO23i{aW$<_DrwddYqD1w3lC2;FML%-HK$WjBI=Qn``G^lT>7Q1tk+<#e0Qq1&^N zGutSKGP#g$D*gG|x@m@8IuP-B5xvWl7atmml;laSW{&@OJMynl2^;=8{(Iw-J>SJp zz%$e4o=Nh{&n3Lep4<(-^i1@WdXl!ks&k`iEr@8Ku9-0#utLX`g#-lbZV#k*LvZ&D z8iA-B1L3(}kN2Vn< zop#bZ=xq&<+v=aHaY2>n@f0)bD+q1f4mfEb(>>=G5;e7F2!FHBF*(p~WqD2^Hu)2F zV^0Jv-_1DMfRBrM$Vq%uOgP!<+HOCXK*sB$^J4yCQTKS+ItMS{LDMK3Dl+BV_)8g6BRgQmDcgkK@p@5d`&2@ePOJ2=39E;9#-&naA?hY^b&-Acq zOa1?u44^dqUm^p4_Qdx+ct20Eb8GRnbLa93SFOgleMcywR99pyG`oJwNo9R2 zgjP@oErQ+`PppI?g~6Jx2M?JkQ<<3X6lg&^;_;rrP z()}6uWdfMVR8+EDBmjVZta(dE=b8dgklywXQ&#RfO(ooYQpUr=7W?I$vD;(E3T6_e zUWYeI8uK`cZcnLbqxTkXiZg%NchOP9x_6aCWkctx>OH{b#(Cbr=ftIdM|^d2;4H^$ zWn(Sh+vfpSv?~>G3*)`C@QcBk4=pe<@OLt>jt5@m7?=@XY95|(*|j%Lh@WJXAKiX# z90etHIE*37NBo_gw3sjRj#ojveQu2A8Ugdiu7&)(R2*g=8Jd>>E~2m%Yrh}@Yaly4+>)Ay zYlT1CZ7lHIFu${dg7K$`;_l`!X<)_Rc8u7^pV&t1mH*x>eE3QPYfF8`1dC*xEaX1O z3G9(*=-XyUW0)zb1_gc1-k(cw0>1zFXg*i;1y*F;ygz8i;1R;GL7ooVXrva5rPb? z5XNE2NCrC8y}X1U%412RR2qd#bkQ_CFs~X&)pJtJhcv$o7w|1{xs0i}v_`+a+ZG9@ zisF6iqVdgsgh~v zwg5L7>TBX@(Q%y12NDSc*~sY;k@>4(-2FbS=)P*<5T-0Nx}3M8ugO4Glj^D5?xZEG|SBa>>UwSD_Rrt!hAXE^9;Y+7gn-OWI!kNYU6Kx(bU07{-ONUegSxl7mfHaeb;qjT5QME0*socju_-zp#8Q zcf`vK5PYp2oPsjL+T-S{)1FwtUcSv%08c<*2XK=;_-wc21YTB=G>Aw zYH zuZ+LQy#;Nxgc&{&2v&Xa(dU+}B_<5t67tYiE@a%8*-{$DNn`Up!ka#ZG zd>g~LES`-0+5f#8=6@0!2;Qt`N&u+!st=xbH^umwb;{L!R!%_%Eo2wXZ9QlH0D{Cb zrquvU6+9DmhES|?1FO6^@Zik0P1FhxYleT>_N*&y>|OtWl|%)JQiGA>E_NiP@!B^4 zJb%_Kul}Y%zB4}x5yEImbJKZ9U(cO(U>DW`%UEGubp^jkZR|rrQdQ zE8Q9~+!GH~gXCG!raEYnA$z9mf~ib=w~30l{unI$s()_h$SSY> zcj1>ZCiR}cL}KNc&?MH_oycV3>C2m`zvU^&m8sZr0aEGMz!I5EH_)(Lwp-bb!f?qv zx9@D&p0LjxT#=;5+<(LxI@<7O<~j>nRX~FU_i=k8?K?{?P0f+UuNm->^(LN#l09}@ zM6r}BR!s`(%M$=pWjPbxi2C*M~Nm1%kBnoq$(~26dgI=VGWjDmLNP}|fTi@T?_B3u)x1(E& z$~&KIlHf3#?U6HdX?0u76Gelili zvQ0{p8^SYbgT78NckwV9@)aiaqxP!+u4s6L`MSr2cJzy6IKqHw1# zGTush*gp~CzAYp?7n71&j0im^D#cq(di)-X2=eq((3k-7c%55DI6KyW^&UZqA%OG# zenR3?Cvj=^XG+L>o&j|54nt*2CT-Wi<|A)t@E5cuGyQop;7JgyTm)L3*@Qlnu!yt8 zZ~ViqDT8>wDC!Q|`lWqeN`fVJN^0Er z-TEd~xz8>J=#y;Nqszd0w1KMm^2-6EoW*tIM5B& z-*23J_hE~^{^~I!nzZ?((R-zemMkekQeNyw9sl?({q#3o`>$NAb6=I^>$jT_G}I^1 z<(2%%0EBKqy11l?B+77}slN4mP2~tR_2w4sgh@{Nq?E42(_IxFBFfJ}EB_%&R| z!OHn2Rn-VpNmUGdtggxG8alt`-kPUC6z4WNs_u9~Y5rf_5+CqT(-wuV#zqN0w^9Q6 z*KjHJavO2Y0s`D9Xs$XQ=ZE?%EWiqw_r{fdyW-!(w;wC|o9n7~&K0RDjl)Kzps`Ev z=ys_?C3^0&W=FhL7ToCg#c8q<&9=9q4i{OimjH0dnPemQtTjjhv0CX;2Bn}J9Lpuy zNbtKVH2FsnV*6?< z9_qHCLA3L^)NtkqgESZ^DNM>w(Lmr(QVv@bBD1^nJ8WcPOmU(BVkL0=6$ zG9--~E@Y)Hsv%4zNt2}R>zedu6fQW9 zSXyhgI@1yON%uIIVi4=TaitYDX=^FWDfwJF;pY;X159)HT;#(800w*$*{uS`WF=Ag zMyl4?Sc*3mdgM(K&9?tiGc%a9yNiVeWfdjn~0f zru@h1wEHg&#s433Z`oF7x^3&?FmNZhGjP{n!QI`1ySpX0ySux)LvVNZ-~@LF2}$-K zRkLQTb=Gz5HFteEe}Hd{_vyX2cHgekYoGZHg12%~Kir+^j8)fiwytO&-OqBvdc&L! zY7S9JAfV4WS!T8^;xn1q%-2Jvd&}!`zg5%<(B%=0v=OQ5F1je*#DNUJ0H*iV+X`JW z45C>e1d>{&@3Nqwz&M01`Mtz^DWEqg?~N=S7iOI5(&YlUt+RVgpnvToT$XcccynkR zNjUEY0q2RQj1n~BW;`%08~HhTGSe2DuIa3gaciTu*X%B1;F)YMT$~rvDd8LkaqWQU zK@J%=pUj}0b7`l|vr1E?FVW3i$m=oB@m|Y?Z_8d{)~%0DmMoryp|V>RkJaSMUU?Cf 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 AG5`Po diff --git a/doc/x11_super_screenshot.png b/doc/x11_super_screenshot.png deleted file mode 100755 index 0cdc3ebc4a860e7406cf1070a58af96cbb18dad2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 244058 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�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�ZxGYL(<_7*U^7BKpmbt+kN%e3D|<%> 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 S_Rkpr0000= 0 and id < len(self.active): - self.active[id] = 0 - else: - debug("Invalid sensor id") - def on(self, id): - if id >= 0 and id < len(self.active): - self.active[id] = 1 - else: - debug("Invalid sensor id") - - def all_off(self): - for i in range(0, len(self.active)): - self.off(i) - def all_on(self): - for i in range(0, len(self.active)): - self.off(i) - - def stop(self): - if self.port != None: #if stop is called before any connection port is not defined (and not connected ) - self.port.close() - wx.PostEvent(self._notify_window, StatusEvent([0,1,"Disconnected"])) - wx.PostEvent(self._notify_window, StatusEvent([2,1,"----"])) - - #class producer end - - def sensor_control_on(self): #after connection enable few buttons - self.settingmenu.Enable(ID_CONFIG,False) - self.settingmenu.Enable(ID_RESET,False) - self.settingmenu.Enable(ID_DISCONNECT,True) - self.dtcmenu.Enable(ID_GETC,True) - self.dtcmenu.Enable(ID_CLEAR,True) - self.GetDTCButton.Enable(True) - self.ClearDTCButton.Enable(True) - - def sensor_toggle(e): - sel = e.m_itemIndex - state = self.senprod.active[sel] - print sel, state - if state == 0: - self.senprod.on(sel) - self.sensors.SetStringItem(sel,1,"1") - elif state == 1: - self.senprod.off(sel) - self.sensors.SetStringItem(sel,1,"0") - else: - debug("Incorrect sensor state") - - self.sensors.Bind(wx.EVT_LIST_ITEM_ACTIVATED,sensor_toggle,id=self.sensor_id) - - def sensor_control_off(self): #after disconnect disable fer buttons - self.dtcmenu.Enable(ID_GETC,False) - self.dtcmenu.Enable(ID_CLEAR,False) - self.settingmenu.Enable(ID_DISCONNECT,False) - self.settingmenu.Enable(ID_CONFIG,True) - self.settingmenu.Enable(ID_RESET,True) - self.GetDTCButton.Enable(False) - self.ClearDTCButton.Enable(False) - #http://pyserial.sourceforge.net/ empty function - #EVT_LIST_ITEM_ACTIVATED(self.sensors,self.sensor_id, lambda : None) - - def build_sensor_page(self): - HOFFSET_LIST=0 - tID = wx.NewId() - self.sensor_id = tID - panel = wx.Panel(self.nb, -1) - - self.sensors = self.MyListCtrl(panel, tID, pos=wx.Point(0,HOFFSET_LIST), - style= - wx.LC_REPORT | - wx.SUNKEN_BORDER | - wx.LC_HRULES | - wx.LC_SINGLE_SEL) - - - self.sensors.InsertColumn(0, "Supported",width=70) - self.sensors.InsertColumn(1, "Sensor",format=wx.LIST_FORMAT_RIGHT, width=250) - self.sensors.InsertColumn(2, "Value") - for i in range(0, len(obd_io.obd_sensors.SENSORS)): - s = obd_io.obd_sensors.SENSORS[i].name - self.sensors.InsertStringItem(i, "") - self.sensors.SetStringItem(i, 1, s) - - - #################################################################### - # This little bit of magic keeps the list the same size as the frame - def OnPSize(e, win = panel): - panel.SetSize(e.GetSize()) - self.sensors.SetSize(e.GetSize()) - w,h = self.frame.GetClientSizeTuple() - self.sensors.SetDimensions(0,HOFFSET_LIST, w-10 , h - 35 ) - - panel.Bind(wx.EVT_SIZE,OnPSize) - #################################################################### - - self.nb.AddPage(panel, "Sensors") - - def build_DTC_page(self): - HOFFSET_LIST=30 #offset from the top of panel (space for buttons) - tID = wx.NewId() - self.DTCpanel = wx.Panel(self.nb, -1) - self.GetDTCButton = wx.Button(self.DTCpanel,-1 ,"Get DTC" , wx.Point(15,0)) - self.ClearDTCButton = wx.Button(self.DTCpanel,-1,"Clear DTC", wx.Point(100,0)) - - #bind functions to button click action - self.DTCpanel.Bind(wx.EVT_BUTTON,self.GetDTC,self.GetDTCButton) - self.DTCpanel.Bind(wx.EVT_BUTTON,self.QueryClear,self.ClearDTCButton) - - self.dtc = self.MyListCtrl(self.DTCpanel,tID, pos=wx.Point(0,HOFFSET_LIST), - style=wx.LC_REPORT|wx.SUNKEN_BORDER|wx.LC_HRULES|wx.LC_SINGLE_SEL) - - self.dtc.InsertColumn(0, "Code", width=100) - self.dtc.InsertColumn(1, "Status",width=100) - self.dtc.InsertColumn(2, "Trouble code") - #################################################################### - # This little bit of magic keeps the list the same size as the frame - def OnPSize(e, win = self.DTCpanel): - self.DTCpanel.SetSize(e.GetSize()) - self.dtc.SetSize(e.GetSize()) - w,h = self.frame.GetClientSizeTuple() - # I have no idea where 70 comes from - self.dtc.SetDimensions(0,HOFFSET_LIST, w-16 , h - 70 ) - - self.DTCpanel.Bind(wx.EVT_SIZE,OnPSize) - #################################################################### - - self.nb.AddPage(self.DTCpanel, "DTC") - - def TraceDebug(self,level,msg): - if self.DEBUGLEVEL<=level: - self.trace.Append([str(level),msg]) - - def OnInit(self): - self.ThreadControl = 0 #say thread what to do - self.COMPORT = 0 - self.senprod = None - self.DEBUGLEVEL = 0 #debug everthing - - tID = wx.NewId() - - #read settings from file - self.config = ConfigParser.RawConfigParser() - - #print platform.system() - #print platform.mac_ver()[] - - if "OS" in os.environ.keys(): #runnig under windows - self.configfilepath="pyobd.ini" - else: - self.configfilepath=os.environ['HOME']+'/.pyobdrc' - if self.config.read(self.configfilepath)==[]: - self.COMPORT="/dev/ttyACM0" - self.RECONNATTEMPTS=5 - self.SERTIMEOUT=2 - else: - self.COMPORT=self.config.get("pyOBD","COMPORT") - self.RECONNATTEMPTS=self.config.getint("pyOBD","RECONNATTEMPTS") - self.SERTIMEOUT=self.config.getint("pyOBD","SERTIMEOUT") - - frame = wx.Frame(None, -1, "pyOBD-II") - self.frame=frame - - EVT_RESULT(self,self.OnResult,EVT_RESULT_ID) - EVT_RESULT(self,self.OnDebug, EVT_DEBUG_ID) - EVT_RESULT(self,self.OnDtc,EVT_DTC_ID) - EVT_RESULT(self,self.OnStatus,EVT_STATUS_ID) - EVT_RESULT(self,self.OnTests,EVT_TESTS_ID) - - # Main notebook frames - self.nb = wx.Notebook(frame, -1, style = wx.NB_TOP) - - self.status = self.MyListCtrl(self.nb, tID,style=wx.LC_REPORT|wx.SUNKEN_BORDER) - self.status.InsertColumn(0, "Description",width=200) - self.status.InsertColumn(1, "Value") - self.status.Append(["Link State","Disconnnected"]); - self.status.Append(["Protocol","---"]); - self.status.Append(["Cable version","---"]); - self.status.Append(["COM port",self.COMPORT]); - - self.nb.AddPage(self.status, "Status") - - self.OBDTests = self.MyListCtrl(self.nb, tID,style=wx.LC_REPORT|wx.SUNKEN_BORDER) - self.OBDTests.InsertColumn(0, "Description",width=200) - self.OBDTests.InsertColumn(1, "Value") - self.nb.AddPage(self.OBDTests, "Tests") - - for i in range(0,len(ptest)): #fill MODE 1 PID 1 test description - self.OBDTests.Append([ptest[i],"---"]); - - self.build_sensor_page() - - self.build_DTC_page() - - self.trace = self.MyListCtrl(self.nb, tID,style=wx.LC_REPORT|wx.SUNKEN_BORDER) - self.trace.InsertColumn(0, "Level",width=40) - self.trace.InsertColumn(1, "Message") - self.nb.AddPage(self.trace, "Trace") - self.TraceDebug(1,"Application started") - - # Setting up the menu. - self.filemenu= wx.Menu() - self.filemenu.Append(ID_EXIT,"E&xit"," Terminate the program") - - self.settingmenu = wx.Menu() - self.settingmenu.Append(ID_CONFIG,"Configure"," Configure pyOBD") - self.settingmenu.Append(ID_RESET,"Connect"," Reopen and connect to device") - self.settingmenu.Append(ID_DISCONNECT,"Disconnect","Close connection to device") - - self.dtcmenu= wx.Menu() - # tady toto nastavi automaticky tab DTC a provede akci - self.dtcmenu.Append(ID_GETC ,"Get DTCs", " Get DTC Codes") - self.dtcmenu.Append(ID_CLEAR ,"Clear DTC", " Clear DTC Codes") - self.dtcmenu.Append(ID_LOOK ,"Code Lookup"," Lookup DTC Codes") - - self.helpmenu = wx.Menu() - - self.helpmenu.Append(ID_HELP_ABOUT ,"About this program", " Get DTC Codes") - self.helpmenu.Append(ID_HELP_VISIT ,"Visit program homepage"," Lookup DTC Codes") - self.helpmenu.Append(ID_HELP_ORDER ,"Order OBD-II cables", " Clear DTC Codes") - - - # Creating the menubar. - self.menuBar = wx.MenuBar() - self.menuBar.Append(self.filemenu,"&File") # Adding the "filemenu" to the MenuBar - self.menuBar.Append(self.settingmenu,"&OBD-II") - self.menuBar.Append(self.dtcmenu,"&Trouble codes") - self.menuBar.Append(self.helpmenu,"&Help") - - frame.SetMenuBar(self.menuBar) # Adding the MenuBar to the Frame content. - - frame.Bind(wx.EVT_MENU,self.OnExit,id=ID_EXIT)# attach the menu-event ID_EXIT to the - frame.Bind(wx.EVT_MENU,self.QueryClear,id=ID_CLEAR) - frame.Bind(wx.EVT_MENU,self.Configure,id=ID_CONFIG) - frame.Bind(wx.EVT_MENU,self.OpenPort,id=ID_RESET) - frame.Bind(wx.EVT_MENU,self.OnDisconnect,id=ID_DISCONNECT) - frame.Bind(wx.EVT_MENU,self.GetDTC,id=ID_GETC) - frame.Bind(wx.EVT_MENU,self.CodeLookup,id=ID_LOOK) - frame.Bind(wx.EVT_MENU,self.OnHelpAbout,id=ID_HELP_ABOUT) - frame.Bind(wx.EVT_MENU,self.OnHelpVisit,id=ID_HELP_VISIT) - frame.Bind(wx.EVT_MENU,self.OnHelpOrder,id=ID_HELP_ORDER) - - self.SetTopWindow(frame) - - frame.Show(True) - frame.SetSize((520,400)) - self.sensor_control_off() - - return True - - def OnHelpVisit(self,event): - webbrowser.open("http://www.obdtester.com/pyobd") - - def OnHelpOrder(self,event): - webbrowser.open("http://www.obdtester.com/order") - - def OnHelpAbout(self,event): #todo about box - Text = """ PyOBD is an automotive OBD2 diagnosting application using ELM237 cable. - -(C) 2008-2009 SeCons Ltd. -(C) 2004 Charles Donour Sizemore - -http://www.obdtester.com/ -http://www.secons.com/ - - PyOBD is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by the Free Software Foundation; -either version 2 of the License, or (at your option) any later version. - - PyOBD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -without even the implied warranty of MEHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -See the GNU General Public License for more details. You should have received a copy of -the GNU General Public License along with PyOBD; if not, write to -the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -""" - - #HelpAboutDlg = wx.Dialog(self.frame, id, title="About") - - - #box = wx.BoxSizer(wx.HORIZONTAL) - #box.Add(wx.StaticText(reconnectPanel,-1,Text,pos=(0,0),size=(200,200))) - #box.Add(wx.Button(HelpAboutDlg,wx.ID_OK),0) - #box.Add(wx.Button(HelpAboutDlg,wx.ID_CANCEL),1) - - #HelpAboutDlg.SetSizer(box) - #HelpAboutDlg.SetAutoLayout(True) - #sizer.Fit(HelpAboutDlg) - #HelpAboutDlg.ShowModal() - - self.HelpAboutDlg = wx.MessageDialog(self.frame, Text, 'About',wx.OK | wx.ICON_INFORMATION) - self.HelpAboutDlg.ShowModal() - self.HelpAboutDlg.Destroy() - - def OnResult(self,event): - self.sensors.SetStringItem(event.data[0], event.data[1], event.data[2]) - - def OnStatus(self,event): - if event.data[0] == 666: #signal, that connection falied - self.sensor_control_off() - else: - self.status.SetStringItem(event.data[0], event.data[1], event.data[2]) - - def OnTests(self,event): - self.OBDTests.SetStringItem(event.data[0], event.data[1], event.data[2]) - - def OnDebug(self,event): - self.TraceDebug(event.data[0],event.data[1]) - - def OnDtc(self,event): - if event.data == 0: #signal, that DTC was cleared - self.dtc.DeleteAllItems() - else: - self.dtc.Append(event.data) - - def OnDisconnect(self,event): #disconnect connection to ECU - self.ThreadControl=666 - self.sensor_control_off() - - def OpenPort(self,e): - - if self.senprod: # signal current producers to finish - self.senprod.stop() - self.ThreadControl = 0 - self.senprod = self.sensorProducer(self,self.COMPORT,self.SERTIMEOUT,self.RECONNATTEMPTS,self.nb) - self.senprod.start() - - self.sensor_control_on() - - def GetDTC(self,e): - self.nb.SetSelection(3) - self.ThreadControl=2 - - def AddDTC(self, code): - self.dtc.InsertStringItem(0, "") - self.dtc.SetStringItem(0, 0, code[0]) - self.dtc.SetStringItem(0, 1, code[1]) - - - def CodeLookup(self,e = None): - id = 0 - diag = wx.Frame(None, id, title="Diagnostic Trouble Codes") - - tree = wx.TreeCtrl(diag, id, style = wx.TR_HAS_BUTTONS) - - root = tree.AddRoot("Code Reference") - proot = tree.AppendItem(root,"Powertrain (P) Codes") - codes = obd_io.pcodes.keys() - codes.sort() - group = "" - for c in codes: - if c[:3] != group: - group_root = tree.AppendItem(proot, c[:3]+"XX") - group = c[:3] - leaf = tree.AppendItem(group_root, c) - tree.AppendItem(leaf, obd_io.pcodes[c]) - - uroot = tree.AppendItem(root,"Network (U) Codes") - codes = obd_io.ucodes.keys() - codes.sort() - group = "" - for c in codes: - if c[:3] != group: - group_root = tree.AppendItem(uroot, c[:3]+"XX") - group = c[:3] - leaf = tree.AppendItem(group_root, c) - tree.AppendItem(leaf, obd_io.ucodes[c]) - - diag.SetSize((400,500)) - diag.Show(True) - - - def QueryClear(self,e): - id = 0 - diag = wx.Dialog(self.frame, id, title="Clear DTC?") - - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(wx.StaticText(diag, -1, "Are you sure you wish to"),0) - sizer.Add(wx.StaticText(diag, -1, "clear all DTC codes and "),0) - sizer.Add(wx.StaticText(diag, -1, "freeze frame data? "),0) - box = wx.BoxSizer(wx.HORIZONTAL) - box.Add(wx.Button(diag,wx.ID_OK, "Ok" ),0) - box.Add(wx.Button(diag,wx.ID_CANCEL, "Cancel"),0) - - sizer.Add(box, 0) - diag.SetSizer(sizer) - diag.SetAutoLayout(True) - sizer.Fit(diag) - r = diag.ShowModal() - if r == wx.ID_OK: - self.ClearDTC() - - def ClearDTC(self): - self.ThreadControl=1 - self.nb.SetSelection(3) - - def Configure(self,e = None): - id = 0 - diag = wx.Dialog(self.frame, id, title="Configure") - sizer = wx.BoxSizer(wx.VERTICAL) - - ports = scanSerial() - rb = wx.RadioBox(diag, id, "Choose Serial Port", - choices = ports, style = wx.RA_SPECIFY_COLS, - majorDimension = 2) - - sizer.Add(rb, 0) - - #timeOut input control - timeoutPanel = wx.Panel(diag, -1) - timeoutCtrl = wx.TextCtrl(timeoutPanel, -1, '',pos=(140,0), size=(35, 25)) - timeoutStatic = wx.StaticText(timeoutPanel,-1,'Timeout:',pos=(3,5),size=(140,20)) - timeoutCtrl.SetValue(str(self.SERTIMEOUT)) - - #reconnect attempt input control - reconnectPanel = wx.Panel(diag, -1) - reconnectCtrl = wx.TextCtrl(reconnectPanel, -1, '',pos=(140,0), size=(35, 25)) - reconnectStatic = wx.StaticText(reconnectPanel,-1,'Reconnect attempts:',pos=(3,5),size=(140,20)) - reconnectCtrl.SetValue(str(self.RECONNATTEMPTS)) - - #web open link button - self.OpenLinkButton = wx.Button(diag,-1,"Click here to order ELM-USB interface",size=(260,30)) - diag.Bind(wx.EVT_BUTTON,self.OnHelpOrder,self.OpenLinkButton) - - #set actual serial port choice - if (self.COMPORT != 0) and (self.COMPORT in ports): - rb.SetSelection(ports.index(self.COMPORT)) - - - sizer.Add(self.OpenLinkButton) - sizer.Add(timeoutPanel,0) - sizer.Add(reconnectPanel,0) - - box = wx.BoxSizer(wx.HORIZONTAL) - box.Add(wx.Button(diag,wx.ID_OK),0) - box.Add(wx.Button(diag,wx.ID_CANCEL),1) - - sizer.Add(box, 0) - diag.SetSizer(sizer) - diag.SetAutoLayout(True) - sizer.Fit(diag) - r = diag.ShowModal() - if r == wx.ID_OK: - - #create section - if self.config.sections()==[]: - self.config.add_section("pyOBD") - #set and save COMPORT - self.COMPORT = ports[rb.GetSelection()] - self.config.set("pyOBD","COMPORT",self.COMPORT) - - #set and save SERTIMEOUT - self.SERTIMEOUT = int(timeoutCtrl.GetValue()) - self.config.set("pyOBD","SERTIMEOUT",self.SERTIMEOUT) - self.status.SetStringItem(3,1,self.COMPORT); - - #set and save RECONNATTEMPTS - self.RECONNATTEMPTS = int(reconnectCtrl.GetValue()) - self.config.set("pyOBD","RECONNATTEMPTS",self.RECONNATTEMPTS) - - #write configuration to cfg file - self.config.write(open(self.configfilepath, 'wb')) - - - def OnExit(self,e = None): - import sys - sys.exit(0) - -app = MyApp(0) -app.MainLoop() diff --git a/pyobd.desktop b/pyobd.desktop deleted file mode 100755 index d396ddd8..00000000 --- a/pyobd.desktop +++ /dev/null @@ -1,10 +0,0 @@ -[Desktop Entry] -Encoding=UTF8 -Icon=/usr/share/pyobd/pyobd.gif -Name=pyOBD: OBD2 Diagnostics -Comment=Car On-Board 2 vehicle diagnostics (ELM-32x compatible interface) -Exec=python /usr/bin/pyobd -Terminal=false -Type=Application -Categories=Utility; -StartupNotify=true diff --git a/pyobd.gif b/pyobd.gif deleted file mode 100755 index 1083d8c9221e70431e52a4c6a16fa20390a7afde..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1288 zcmV+j1^4<#Nk%w1VITk?0OkMy|NsB~*v#B^QvTuA{=23B{{R1uY0Ob6^Iu5gyfK~jhl>Uro{L8!k_3_kID&&%O z{)l1x@a+Dfg8qhG?YyhdQ7PeBEa7=p|AJWl{`>uva@|-f(Nrtqi*V#yF#m2o|DAmO z?&$opo9kXM{Qv*-Z$;r|KL2(|{&7O)TrK{ojsA5-{lKjMievtwh}Kms-dQiq z^I|Xm#O$ zgjn!|At@xaX}UN8Oq{r6`#`eQWrV>A9^Gx4E*A^8LV00000EC2ui z03ZM$000O7fB=FeT?q*Q1rtUCI5r*^IUXzo0ag1qqT0V2m86BefDF%1{AGks^Hh>|t1dKo$-hvaoW1qZO=zGcvB25d}jLEn0pcKwx5m zl~x@8(8#r*h9I(J^Hv~4vLb_=A5aiLS+b->01`(&4E&J6!!d91!lqe4B?uBfIs!mx z(qzetB{*D=7(JSj0X!yB16vh$PMA3{96ZSL;)R474_Guf=3zjF0km*M6NGDm(nD&1 zV~8L@f{Hdryb&PMB8)r?x@_4>K%~qB3xvgFnDC&64;2k&%;*5X2Dm8<^ca$s=un3| z5?<7ZQ{YSwA6OXVKo=fpq?aDLd_qJC0H~p#4?ai%KpiLWZ~_A~&=AE00noq(6&4Iv zi*Y0pQJxPnsF8paPdstL1qgY76eDIJl^}7s5ODsWRlDO zjYcd8K$1!Vkii{0>`+A#mLb7{4q#kTfCEf~`3Dn1lvyT&qu3!t3L0pjKpYt;aaJFE z1dvG?ICxfD4Jz~?f`t$u0ze#I0D%GuUpPk_}BDpyzEh)F6Tg zC&;P*tr`Ha#it<*vFir;Bw&RYFVLo+r9JRq;j*;8!K<`G4B^BOZqQ=LGcClhtqLNj zkgNg- y8_-KDzCcJ@@)}h@5Dk*W4J-&bOJ;CI3IcHPMF?I9Pz4AbOfZIVn~;-0AOJga3OXJD From 32d9ac03c163469e537c6caeb55aa22bd7654c2e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 14 Oct 2014 23:26:01 -0400 Subject: [PATCH 008/569] sorry cowfish, here's a commit just for you --- cowfish.png | Bin 56721 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100755 cowfish.png diff --git a/cowfish.png b/cowfish.png deleted file mode 100755 index 4c6c1fe6710ee713258182d3b93d7297c0a234e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56721 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

-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 Date: Tue, 14 Oct 2014 23:38:53 -0400 Subject: [PATCH 009/569] updated GPL license file --- COPYING => LICENSE | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) rename COPYING => LICENSE (95%) mode change 100755 => 100644 diff --git a/COPYING b/LICENSE old mode 100755 new mode 100644 similarity index 95% rename from COPYING rename to LICENSE index 5b6e7c66..d6a93266 --- a/COPYING +++ b/LICENSE @@ -1,12 +1,12 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 - Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - Preamble + Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public @@ -15,7 +15,7 @@ software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by -the GNU Library General Public License instead.) You can apply it to +the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not @@ -55,8 +55,8 @@ patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. - - GNU GENERAL PUBLIC LICENSE + + GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains @@ -110,7 +110,7 @@ above, provided that you also meet all of these conditions: License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) - + These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in @@ -168,7 +168,7 @@ access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. - + 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is @@ -225,7 +225,7 @@ impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. - + 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License @@ -255,7 +255,7 @@ make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. - NO WARRANTY + NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN @@ -277,9 +277,9 @@ YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it @@ -290,8 +290,8 @@ to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - Copyright (C) + {description} + Copyright (C) {year} {fullname} This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -303,10 +303,9 @@ the "copyright" line and a pointer to where the full notice is found. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. @@ -330,11 +329,12 @@ necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. - , 1 April 1989 + {signature of Ty Coon}, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Library General +library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. + From 71cbe76430e3395c81e80c546d43cb2b065d4b07 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 14 Oct 2014 23:41:30 -0400 Subject: [PATCH 010/569] made a module --- pyobd/__init__.py | 0 debugEvent.py => pyobd/debugEvent.py | 0 obd2_codes.py => pyobd/obd2_codes.py | 0 obd_capture.py => pyobd/obd_capture.py | 0 obd_io.py => pyobd/obd_io.py | 0 obd_recorder.py => pyobd/obd_recorder.py | 0 obd_sensors.py => pyobd/obd_sensors.py | 0 obd_utils.py => pyobd/obd_utils.py | 0 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 pyobd/__init__.py rename debugEvent.py => pyobd/debugEvent.py (100%) mode change 100755 => 100644 rename obd2_codes.py => pyobd/obd2_codes.py (100%) mode change 100755 => 100644 rename obd_capture.py => pyobd/obd_capture.py (100%) mode change 100755 => 100644 rename obd_io.py => pyobd/obd_io.py (100%) mode change 100755 => 100644 rename obd_recorder.py => pyobd/obd_recorder.py (100%) mode change 100755 => 100644 rename obd_sensors.py => pyobd/obd_sensors.py (100%) mode change 100755 => 100644 rename obd_utils.py => pyobd/obd_utils.py (100%) mode change 100755 => 100644 diff --git a/pyobd/__init__.py b/pyobd/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/debugEvent.py b/pyobd/debugEvent.py old mode 100755 new mode 100644 similarity index 100% rename from debugEvent.py rename to pyobd/debugEvent.py diff --git a/obd2_codes.py b/pyobd/obd2_codes.py old mode 100755 new mode 100644 similarity index 100% rename from obd2_codes.py rename to pyobd/obd2_codes.py diff --git a/obd_capture.py b/pyobd/obd_capture.py old mode 100755 new mode 100644 similarity index 100% rename from obd_capture.py rename to pyobd/obd_capture.py diff --git a/obd_io.py b/pyobd/obd_io.py old mode 100755 new mode 100644 similarity index 100% rename from obd_io.py rename to pyobd/obd_io.py diff --git a/obd_recorder.py b/pyobd/obd_recorder.py old mode 100755 new mode 100644 similarity index 100% rename from obd_recorder.py rename to pyobd/obd_recorder.py diff --git a/obd_sensors.py b/pyobd/obd_sensors.py old mode 100755 new mode 100644 similarity index 100% rename from obd_sensors.py rename to pyobd/obd_sensors.py diff --git a/obd_utils.py b/pyobd/obd_utils.py old mode 100755 new mode 100644 similarity index 100% rename from obd_utils.py rename to pyobd/obd_utils.py From 551753c799a08f57cb0735784c800a0e19f09c48 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 14 Oct 2014 23:58:12 -0400 Subject: [PATCH 011/569] removed recorder and WX debug events --- pyobd/debugEvent.py | 45 ------------------ pyobd/obd_io.py | 19 ++++---- pyobd/obd_recorder.py | 103 ------------------------------------------ pyobd/obd_utils.py | 1 - 4 files changed, 9 insertions(+), 159 deletions(-) delete mode 100644 pyobd/debugEvent.py delete mode 100644 pyobd/obd_recorder.py diff --git a/pyobd/debugEvent.py b/pyobd/debugEvent.py deleted file mode 100644 index 555ed234..00000000 --- a/pyobd/debugEvent.py +++ /dev/null @@ -1,45 +0,0 @@ - #!/usr/bin/env python -########################################################################### -# obd_sensors.py -# -# Copyright 2004 Donour Sizemore (donour@uchicago.edu) -# Copyright 2009 Secons Ltd. (www.obdtester.com) -# -# This file is part of pyOBD. -# -# pyOBD is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# pyOBD is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with pyOBD; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -########################################################################### -try: - import wx - - EVT_DEBUG_ID = 1010 - - def debug_display(window, position, message): - if window is None: - print message - else: - wx.PostEvent(window, DebugEvent([position, message])) - - class DebugEvent(wx.PyEvent): - """Simple event to carry arbitrary result data.""" - def __init__(self, data): - """Init Result Event.""" - wx.PyEvent.__init__(self) - self.SetEventType(EVT_DEBUG_ID) - self.data = data -except ImportError as e: - def debug_display(window, position, message): - print message - diff --git a/pyobd/obd_io.py b/pyobd/obd_io.py index 91bf078f..90876077 100644 --- a/pyobd/obd_io.py +++ b/pyobd/obd_io.py @@ -36,7 +36,6 @@ CLEAR_DTC_COMMAND = "04" GET_FREEZE_DTC_COMMAND = "07" -from debugEvent import debug_display #__________________________________________________________________________ def decrypt_dtc_code(code): @@ -84,7 +83,7 @@ def __init__(self,portnum,_notify_window,SERTIMEOUT,RECONNATTEMPTS): self.port = None self._notify_window=_notify_window - debug_display(self._notify_window, 1, "Opening interface (serial port)") + print "Opening interface (serial port)" try: self.port = serial.Serial(portnum,baud, \ @@ -95,8 +94,8 @@ def __init__(self,portnum,_notify_window,SERTIMEOUT,RECONNATTEMPTS): self.State = 0 return None - debug_display(self._notify_window, 1, "Interface successfully " + self.port.portstr + " opened") - debug_display(self._notify_window, 1, "Connecting to ECU...") + print "Interface successfully " + self.port.portstr + " opened" + print "Connecting to ECU..." try: self.send_command("atz") # initialize @@ -110,9 +109,9 @@ def __init__(self,portnum,_notify_window,SERTIMEOUT,RECONNATTEMPTS): self.State = 0 return None - debug_display(self._notify_window, 2, "atz response:" + self.ELMver) + print "atz response:" + self.ELMver self.send_command("ate0") # echo off - debug_display(self._notify_window, 2, "ate0 response:" + self.get_result()) + print "ate0 response:" + self.get_result() self.send_command("0100") ready = self.get_result() @@ -120,7 +119,7 @@ def __init__(self,portnum,_notify_window,SERTIMEOUT,RECONNATTEMPTS): self.State = 0 return None - debug_display(self._notify_window, 2, "0100 response:" + ready) + print "0100 response:" + ready return None def close(self): @@ -141,7 +140,7 @@ def send_command(self, cmd): for c in cmd: self.port.write(c) self.port.write("\r\n") - #debug_display(self._notify_window, 3, "Send command:" + cmd) + #print "Send command:" + cmd def interpret_result(self,code): """Internal use only: not a public interface""" @@ -194,12 +193,12 @@ def get_result(self): if buffer != "" or c != ">": #if something is in buffer, add everything buffer = buffer + c - #debug_display(self._notify_window, 3, "Get result:" + buffer) + #print(self._notify_window, 3, "Get result:" + buffer) if(buffer == ""): return None return buffer else: - debug_display(self._notify_window, 3, "NO self.port!") + print "NO self.port!" return None # get sensor value from command diff --git a/pyobd/obd_recorder.py b/pyobd/obd_recorder.py deleted file mode 100644 index a0e0f91d..00000000 --- a/pyobd/obd_recorder.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python - -import obd_io -import serial -import platform -import obd_sensors -from datetime import datetime -import time -import getpass - - -from obd_utils import scanSerial - -class OBD_Recorder(): - def __init__(self, path, log_items): - self.port = None - self.sensorlist = [] - localtime = time.localtime(time.time()) - filename = path+"car-"+str(localtime[0])+"-"+str(localtime[1])+"-"+str(localtime[2])+"-"+str(localtime[3])+"-"+str(localtime[4])+"-"+str(localtime[5])+".log" - self.log_file = open(filename, "w", 128) - self.log_file.write("Time,RPM,MPH,Throttle,Load,Fuel Status\n"); - - for item in log_items: - self.add_log_item(item) - - self.gear_ratios = [34/13, 39/21, 36/23, 27/20, 26/21, 25/22] - #log_formatter = logging.Formatter('%(asctime)s.%(msecs).03d,%(message)s', "%H:%M:%S") - - def connect(self): - portnames = scanSerial() - #portnames = ['COM10'] - print portnames - for port in portnames: - self.port = obd_io.OBDPort(port, None, 2, 2) - if(self.port.State == 0): - self.port.close() - self.port = None - else: - break - - if(self.port): - print "Connected to "+self.port.port.name - - def is_connected(self): - return self.port - - def add_log_item(self, item): - for index, e in enumerate(obd_sensors.SENSORS): - if(item == e.shortname): - self.sensorlist.append(index) - print "Logging item: "+e.name - break - - - def record_data(self): - if(self.port is None): - return None - - print "Logging started" - - while 1: - localtime = datetime.now() - current_time = str(localtime.hour)+":"+str(localtime.minute)+":"+str(localtime.second)+"."+str(localtime.microsecond) - log_string = current_time - results = {} - for index in self.sensorlist: - (name, value, unit) = self.port.sensor(index) - log_string = log_string + ","+str(value) - results[obd_sensors.SENSORS[index].shortname] = value; - - gear = self.calculate_gear(results["rpm"], results["speed"]) - log_string = log_string #+ "," + str(gear) - self.log_file.write(log_string+"\n") - - - def calculate_gear(self, rpm, speed): - if speed == "" or speed == 0: - return 0 - if rpm == "" or rpm == 0: - return 0 - - rps = rpm/60 - mps = (speed*1.609*1000)/3600 - - primary_gear = 85/46 #street triple - final_drive = 47/16 - - tyre_circumference = 1.978 #meters - - current_gear_ratio = (rps*tyre_circumference)/(mps*primary_gear*final_drive) - - #print current_gear_ratio - gear = min((abs(current_gear_ratio - i), i) for i in self.gear_ratios)[1] - return gear - -username = getpass.getuser() -logitems = ["rpm", "speed", "throttle_pos", "load", "fuel_status"] -o = OBD_Recorder('/home/'+username+'/pyobd-pi/log/', logitems) -o.connect() - -if not o.is_connected(): - print "Not connected" -o.record_data() diff --git a/pyobd/obd_utils.py b/pyobd/obd_utils.py index 9e8aadb1..26517854 100644 --- a/pyobd/obd_utils.py +++ b/pyobd/obd_utils.py @@ -1,5 +1,4 @@ import serial -import platform import errno # returns boolean for port availability From c51f1a0c6c551af10cf37fee1358c889a8ac8bb2 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 00:12:21 -0400 Subject: [PATCH 012/569] moved utility functions to utils file --- pyobd/obd_io.py | 50 +++++++++----------------------------------- pyobd/obd_sensors.py | 6 +++--- pyobd/obd_utils.py | 42 ++++++++++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 44 deletions(-) diff --git a/pyobd/obd_io.py b/pyobd/obd_io.py index 90876077..5245ff4e 100644 --- a/pyobd/obd_io.py +++ b/pyobd/obd_io.py @@ -25,52 +25,20 @@ import serial import string import time -from math import ceil -from datetime import datetime import obd_sensors +from obd_utils import hex_to_int -from obd_sensors import hex_to_int GET_DTC_COMMAND = "03" CLEAR_DTC_COMMAND = "04" GET_FREEZE_DTC_COMMAND = "07" -#__________________________________________________________________________ -def decrypt_dtc_code(code): - """Returns the 5-digit DTC code from hex encoding""" - dtc = [] - current = code - for i in range(0,3): - if len(current)<4: - raise "Tried to decode bad DTC: %s" % code - - tc = obd_sensors.hex_to_int(current[0]) #typecode - tc = tc >> 2 - if tc == 0: - type = "P" - elif tc == 1: - type = "C" - elif tc == 2: - type = "B" - elif tc == 3: - type = "U" - else: - raise tc - - dig1 = str(obd_sensors.hex_to_int(current[0]) & 3) - dig2 = str(obd_sensors.hex_to_int(current[1])) - dig3 = str(obd_sensors.hex_to_int(current[2])) - dig4 = str(obd_sensors.hex_to_int(current[3])) - dtc.append(type+dig1+dig2+dig3+dig4) - current = current[4:] - return dtc -#__________________________________________________________________________ class OBDPort: """ OBDPort abstracts all communication with OBD-II device.""" - def __init__(self,portnum,_notify_window,SERTIMEOUT,RECONNATTEMPTS): + def __init__(self, portnum, SERTIMEOUT, RECONNATTEMPTS): """Initializes port by resetting device and gettings supported PIDs. """ # These should really be set by the user. baud = 38400 @@ -81,13 +49,16 @@ def __init__(self,portnum,_notify_window,SERTIMEOUT,RECONNATTEMPTS): self.ELMver = "Unknown" self.State = 1 #state SERIAL is 1 connected, 0 disconnected (connection failed) self.port = None - - self._notify_window=_notify_window + print "Opening interface (serial port)" try: - self.port = serial.Serial(portnum,baud, \ - parity = par, stopbits = sb, bytesize = databits,timeout = to) + self.port = serial.Serial(portnum, \ + baud, \ + parity = par, \ + stopbits = sb, \ + bytesize = databits, \ + timeout = to) except serial.SerialException as e: print e @@ -193,7 +164,7 @@ def get_result(self): if buffer != "" or c != ">": #if something is in buffer, add everything buffer = buffer + c - #print(self._notify_window, 3, "Get result:" + buffer) + #print "Get result:" + buffer if(buffer == ""): return None return buffer @@ -322,4 +293,3 @@ def log(self, sensor_index, filename): line = "%.6f,\t%s\n" % (now - start_time, data[1]) file.write(line) file.flush() - diff --git a/pyobd/obd_sensors.py b/pyobd/obd_sensors.py index 24dbc802..ee18f10f 100644 --- a/pyobd/obd_sensors.py +++ b/pyobd/obd_sensors.py @@ -22,9 +22,9 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ########################################################################### -def hex_to_int(str): - i = eval("0x" + str, {}, {}) - return i + +from obd_utils import hex_to_int + def maf(code): code = hex_to_int(code) diff --git a/pyobd/obd_utils.py b/pyobd/obd_utils.py index 26517854..b49b4b62 100644 --- a/pyobd/obd_utils.py +++ b/pyobd/obd_utils.py @@ -1,8 +1,9 @@ import serial import errno -# returns boolean for port availability + def tryPort(portStr): + """returns boolean for port availability""" try: s = serial.Serial(portStr) s.close() # explicit close 'cause of delayed GC in java @@ -17,9 +18,11 @@ def tryPort(portStr): return False + def scanSerial(): """scan for available ports. return a list of serial names""" available = [] + # Enable Bluetooh connection for i in range(10): portStr = "/dev/rfcomm%d" % i @@ -41,3 +44,40 @@ def scanSerial(): ''' return available + + + +def hex_to_int(str): + i = eval("0x" + str, {}, {}) + return i + + + +def decrypt_dtc_code(code): + """Returns the 5-digit DTC code from hex encoding""" + dtc = [] + current = code + for i in range(0,3): + if len(current)<4: + raise "Tried to decode bad DTC: %s" % code + + tc = hex_to_int(current[0]) #typecode + tc = tc >> 2 + if tc == 0: + type = "P" + elif tc == 1: + type = "C" + elif tc == 2: + type = "B" + elif tc == 3: + type = "U" + else: + raise tc + + dig1 = str(hex_to_int(current[0]) & 3) + dig2 = str(hex_to_int(current[1])) + dig3 = str(hex_to_int(current[2])) + dig4 = str(hex_to_int(current[3])) + dtc.append(type+dig1+dig2+dig3+dig4) + current = current[4:] + return dtc From fca1918a90926d864dfc9cbe9cd5c800226aa448 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 00:25:27 -0400 Subject: [PATCH 013/569] added connection state enum, and tweaked formatting --- pyobd/obd_capture.py | 2 +- pyobd/obd_io.py | 31 +++++++++++++++++++------------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/pyobd/obd_capture.py b/pyobd/obd_capture.py index 023515a8..4cf08a4b 100644 --- a/pyobd/obd_capture.py +++ b/pyobd/obd_capture.py @@ -19,7 +19,7 @@ def connect(self): portnames = scanSerial() print portnames for port in portnames: - self.port = obd_io.OBDPort(port, None, 2, 2) + self.port = obd_io.OBDPort(port, 2, 2) if(self.port.State == 0): self.port.close() self.port = None diff --git a/pyobd/obd_io.py b/pyobd/obd_io.py index 5245ff4e..ada780af 100644 --- a/pyobd/obd_io.py +++ b/pyobd/obd_io.py @@ -35,25 +35,32 @@ GET_FREEZE_DTC_COMMAND = "07" +class State(): + """ Enum for connection states """ + Unconnected, Connected = range(2) + class OBDPort: """ OBDPort abstracts all communication with OBD-II device.""" - def __init__(self, portnum, SERTIMEOUT, RECONNATTEMPTS): + + def __init__(self, portname, timeout): """Initializes port by resetting device and gettings supported PIDs. """ + # These should really be set by the user. baud = 38400 databits = 8 par = serial.PARITY_NONE # parity sb = 1 # stop bits - to = SERTIMEOUT + to = timeout + self.ELMver = "Unknown" - self.State = 1 #state SERIAL is 1 connected, 0 disconnected (connection failed) - self.port = None + self.state = State.Connected + self.port = None print "Opening interface (serial port)" try: - self.port = serial.Serial(portnum, \ + self.port = serial.Serial(portname, \ baud, \ parity = par, \ stopbits = sb, \ @@ -62,7 +69,7 @@ def __init__(self, portnum, SERTIMEOUT, RECONNATTEMPTS): except serial.SerialException as e: print e - self.State = 0 + self.state = State.Unconnected return None print "Interface successfully " + self.port.portstr + " opened" @@ -72,12 +79,12 @@ def __init__(self, portnum, SERTIMEOUT, RECONNATTEMPTS): self.send_command("atz") # initialize time.sleep(1) except serial.SerialException: - self.State = 0 + self.state = State.Unconnected return None self.ELMver = self.get_result() - if(self.ELMver is None): - self.State = 0 + if self.ELMver is None : + self.state = State.Unconnected return None print "atz response:" + self.ELMver @@ -86,8 +93,8 @@ def __init__(self, portnum, SERTIMEOUT, RECONNATTEMPTS): self.send_command("0100") ready = self.get_result() - if(ready is None): - self.State = 0 + if ready is None: + self.state = State.Unconnected return None print "0100 response:" + ready @@ -96,7 +103,7 @@ def __init__(self, portnum, SERTIMEOUT, RECONNATTEMPTS): def close(self): """ Resets device and closes all associated filehandles""" - if (self.port!= None) and self.State==1: + if (self.port != None) and self.state == State.Connected: self.send_command("atz") self.port.close() From 4e14a72d4fe80980b5f46d2b213ad96ae4563a5c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 01:15:50 -0400 Subject: [PATCH 014/569] fixed whitespace and added parameter for specific port name --- pyobd/obd_capture.py | 40 ++-- pyobd/obd_io.py | 529 ++++++++++++++++++++++--------------------- pyobd/obd_utils.py | 2 +- 3 files changed, 295 insertions(+), 276 deletions(-) diff --git a/pyobd/obd_capture.py b/pyobd/obd_capture.py index 4cf08a4b..8daf6cfb 100644 --- a/pyobd/obd_capture.py +++ b/pyobd/obd_capture.py @@ -1,6 +1,7 @@ #!/usr/bin/env python -import obd_io +from obd_io import OBDPort +from obd_io import State import serial import platform import obd_sensors @@ -10,27 +11,36 @@ from obd_utils import scanSerial class OBD_Capture(): + def __init__(self): self.supportedSensorList = [] self.port = None localtime = time.localtime(time.time()) - def connect(self): - portnames = scanSerial() - print portnames - for port in portnames: - self.port = obd_io.OBDPort(port, 2, 2) - if(self.port.State == 0): - self.port.close() - self.port = None - else: - break + def connect(self, portstr=None): + """ attempts to instantiate an OBDPort object. Return boolean for success/failure""" + + if portstr is None: + portnames = scanSerial() + print portnames + + for port in portnames: - if(self.port): - print "Connected to "+self.port.port.name + self.port = OBDPort(port) + + if(self.port.state == State.Connected): + # success! stop searching for serial + break + else: + self.port = OBDPort(portstr) + + return self.is_connected() def is_connected(self): - return self.port + return (self.port is not None) and (self.port.state == State.Connected) + + def get_port_name(self): + return self.port.port.name def getSupportedSensorList(self): return self.supportedSensorList @@ -74,6 +84,7 @@ def capture_data(self): return text + if __name__ == "__main__": o = OBD_Capture() @@ -82,4 +93,5 @@ def capture_data(self): if not o.is_connected(): print "Not connected" else: + print "Connected to " + o.get_port_name() o.capture_data() diff --git a/pyobd/obd_io.py b/pyobd/obd_io.py index ada780af..7e09e774 100644 --- a/pyobd/obd_io.py +++ b/pyobd/obd_io.py @@ -36,267 +36,274 @@ class State(): - """ Enum for connection states """ - Unconnected, Connected = range(2) + """ Enum for connection states """ + Unconnected, Connected = range(2) class OBDPort: - """ OBDPort abstracts all communication with OBD-II device.""" - - def __init__(self, portname, timeout): - """Initializes port by resetting device and gettings supported PIDs. """ - - # These should really be set by the user. - baud = 38400 - databits = 8 - par = serial.PARITY_NONE # parity - sb = 1 # stop bits - to = timeout - - self.ELMver = "Unknown" - self.state = State.Connected - self.port = None - - print "Opening interface (serial port)" - - try: - self.port = serial.Serial(portname, \ - baud, \ - parity = par, \ - stopbits = sb, \ - bytesize = databits, \ - timeout = to) - - except serial.SerialException as e: - print e - self.state = State.Unconnected - return None - - print "Interface successfully " + self.port.portstr + " opened" - print "Connecting to ECU..." - - try: - self.send_command("atz") # initialize - time.sleep(1) - except serial.SerialException: - self.state = State.Unconnected - return None - - self.ELMver = self.get_result() - if self.ELMver is None : - self.state = State.Unconnected - return None - - print "atz response:" + self.ELMver - self.send_command("ate0") # echo off - print "ate0 response:" + self.get_result() - self.send_command("0100") - ready = self.get_result() - - if ready is None: - self.state = State.Unconnected - return None - - print "0100 response:" + ready - return None - - def close(self): - """ Resets device and closes all associated filehandles""" - - if (self.port != None) and self.state == State.Connected: - self.send_command("atz") - self.port.close() - - self.port = None - self.ELMver = "Unknown" - - def send_command(self, cmd): - """Internal use only: not a public interface""" - if self.port: - self.port.flushOutput() - self.port.flushInput() - for c in cmd: - self.port.write(c) - self.port.write("\r\n") - #print "Send command:" + cmd - - def interpret_result(self,code): - """Internal use only: not a public interface""" - # Code will be the string returned from the device. - # It should look something like this: - # '41 11 0 0\r\r' - - # 9 seems to be the length of the shortest valid response - if len(code) < 7: - #raise Exception("BogusCode") - print "boguscode?"+code - - # get the first thing returned, echo should be off - code = string.split(code, "\r") - code = code[0] - - #remove whitespace - code = string.split(code) - code = string.join(code, "") - - #cables can behave differently - if code[:6] == "NODATA": # there is no such sensor - return "NODATA" - - # first 4 characters are code from ELM - code = code[4:] - return code - - def get_result(self): - """Internal use only: not a public interface""" - #time.sleep(0.01) - repeat_count = 0 - if self.port is not None: - buffer = "" - while 1: - c = self.port.read(1) - if len(c) == 0: - if(repeat_count == 5): - break - print "Got nothing\n" - repeat_count = repeat_count + 1 - continue - - if c == '\r': - continue - - if c == ">": - break; - - if buffer != "" or c != ">": #if something is in buffer, add everything - buffer = buffer + c - - #print "Get result:" + buffer - if(buffer == ""): - return None - return buffer - else: - print "NO self.port!" - return None - - # get sensor value from command - def get_sensor_value(self,sensor): - """Internal use only: not a public interface""" - cmd = sensor.cmd - self.send_command(cmd) - data = self.get_result() - - if data: - data = self.interpret_result(data) - if data != "NODATA": - data = sensor.value(data) - else: - return "NORESPONSE" - - return data - - # return string of sensor name and value from sensor index - def sensor(self , sensor_index): - """Returns 3-tuple of given sensors. 3-tuple consists of - (Sensor Name (string), Sensor Value (string), Sensor Unit (string) ) """ - sensor = obd_sensors.SENSORS[sensor_index] - r = self.get_sensor_value(sensor) - return (sensor.name,r, sensor.unit) - - def sensor_names(self): - """Internal use only: not a public interface""" - names = [] - for s in obd_sensors.SENSORS: - names.append(s.name) - return names - - def get_tests_MIL(self): - statusText=["Unsupported","Supported - Completed","Unsupported","Supported - Incompleted"] - - statusRes = self.sensor(1)[1] #GET values - statusTrans = [] #translate values to text - - statusTrans.append(str(statusRes[0])) #DTCs - - if statusRes[1]==0: #MIL - statusTrans.append("Off") - else: - statusTrans.append("On") - - for i in range(2,len(statusRes)): #Tests - statusTrans.append(statusText[statusRes[i]]) - - return statusTrans - - # - # fixme: j1979 specifies that the program should poll until the number - # of returned DTCs matches the number indicated by a call to PID 01 - # - def get_dtc(self): - """Returns a list of all pending DTC codes. Each element consists of - a 2-tuple: (DTC code (string), Code description (string) )""" - dtcLetters = ["P", "C", "B", "U"] - r = self.sensor(1)[1] #data - dtcNumber = r[0] - mil = r[1] - DTCCodes = [] - - - print "Number of stored DTC:" + str(dtcNumber) + " MIL: " + str(mil) - # get all DTC, 3 per mesg response - for i in range(0, ((dtcNumber+2)/3)): - self.send_command(GET_DTC_COMMAND) - res = self.get_result() - print "DTC result:" + res - for i in range(0, 3): - val1 = hex_to_int(res[3+i*6:5+i*6]) - val2 = hex_to_int(res[6+i*6:8+i*6]) #get DTC codes from response (3 DTC each 2 bytes) - val = (val1<<8)+val2 #DTC val as int - - if val==0: #skip fill of last packet - break - - DTCStr=dtcLetters[(val&0xC000)>14]+str((val&0x3000)>>12)+str((val&0x0f00)>>8)+str((val&0x00f0)>>4)+str(val&0x000f) - - DTCCodes.append(["Active",DTCStr]) - - #read mode 7 - self.send_command(GET_FREEZE_DTC_COMMAND) - res = self.get_result() - - if res[:7] == "NODATA": #no freeze frame - return DTCCodes - - print "DTC freeze result:" + res - for i in range(0, 3): - val1 = hex_to_int(res[3+i*6:5+i*6]) - val2 = hex_to_int(res[6+i*6:8+i*6]) #get DTC codes from response (3 DTC each 2 bytes) - val = (val1<<8)+val2 #DTC val as int - - if val==0: #skip fill of last packet - break - - DTCStr=dtcLetters[(val&0xC000)>14]+str((val&0x3000)>>12)+str((val&0x0f00)>>8)+str((val&0x00f0)>>4)+str(val&0x000f) - DTCCodes.append(["Passive",DTCStr]) - - return DTCCodes - - def clear_dtc(self): - """Clears all DTCs and freeze frame data""" - self.send_command(CLEAR_DTC_COMMAND) - r = self.get_result() - return r - - def log(self, sensor_index, filename): - file = open(filename, "w") - start_time = time.time() - if file: - data = self.sensor(sensor_index) - file.write("%s \t%s(%s)\n" % \ - ("Time", string.strip(data[0]), data[2])) - while 1: - now = time.time() - data = self.sensor(sensor_index) - line = "%.6f,\t%s\n" % (now - start_time, data[1]) - file.write(line) - file.flush() + """ OBDPort abstracts all communication with OBD-II device.""" + + def __init__(self, portname): + """Initializes port by resetting device and gettings supported PIDs. """ + + # These should really be set by the user. + baud = 38400 + databits = 8 + parity = serial.PARITY_NONE + stopbits = 1 + timeout = 2 #seconds + + self.ELMver = "Unknown" + self.state = State.Connected + self.port = None + + print "Opening interface (serial port)" + + try: + self.port = serial.Serial(portname, \ + baud, \ + parity = parity, \ + stopbits = stopbits, \ + bytesize = databits, \ + timeout = timeout) + + except serial.SerialException as e: + error(e) + return None + + print "Interface successfully " + self.port.portstr + " opened" + print "Connecting to ECU..." + + try: + self.send_command("atz") # initialize + time.sleep(1) + except serial.SerialException as e: + error(e) + return None + + self.ELMver = self.get_result() + if self.ELMver is None : + error("ELMver returned None") + return None + + print "atz response:" + self.ELMver + self.send_command("ate0") # echo off + print "ate0 response:" + self.get_result() + self.send_command("0100") + ready = self.get_result() + + if ready is None: + self.state = State.Unconnected + return None + + print "0100 response:" + ready + return None + + def error(self, msg=None): + """ called when connection error has been encountered """ + print "Connection Error:" + if msg is not None: + print msg + self.port.close() + self.state = State.Unconnected + + + def close(self): + """ Resets device and closes all associated filehandles""" + + if (self.port != None) and self.state == State.Connected: + self.send_command("atz") + self.port.close() + + self.port = None + self.ELMver = "Unknown" + + def send_command(self, cmd): + """Internal use only: not a public interface""" + if self.port: + self.port.flushOutput() + self.port.flushInput() + for c in cmd: + self.port.write(c) + self.port.write("\r\n") + #print "Send command:" + cmd + + def interpret_result(self,code): + """Internal use only: not a public interface""" + # Code will be the string returned from the device. + # It should look something like this: + # '41 11 0 0\r\r' + + # 9 seems to be the length of the shortest valid response + if len(code) < 7: + #raise Exception("BogusCode") + print "boguscode?"+code + + # get the first thing returned, echo should be off + code = string.split(code, "\r") + code = code[0] + + #remove whitespace + code = string.split(code) + code = string.join(code, "") + + #cables can behave differently + if code[:6] == "NODATA": # there is no such sensor + return "NODATA" + + # first 4 characters are code from ELM + code = code[4:] + return code + + def get_result(self): + """Internal use only: not a public interface""" + #time.sleep(0.01) + repeat_count = 0 + if self.port is not None: + buffer = "" + while 1: + c = self.port.read(1) + if len(c) == 0: + if(repeat_count == 5): + break + print "Got nothing\n" + repeat_count = repeat_count + 1 + continue + + if c == '\r': + continue + + if c == ">": + break; + + if buffer != "" or c != ">": #if something is in buffer, add everything + buffer = buffer + c + + #print "Get result:" + buffer + if(buffer == ""): + return None + return buffer + else: + print "NO self.port!" + return None + + # get sensor value from command + def get_sensor_value(self,sensor): + """Internal use only: not a public interface""" + cmd = sensor.cmd + self.send_command(cmd) + data = self.get_result() + + if data: + data = self.interpret_result(data) + if data != "NODATA": + data = sensor.value(data) + else: + return "NORESPONSE" + + return data + + # return string of sensor name and value from sensor index + def sensor(self , sensor_index): + """Returns 3-tuple of given sensors. 3-tuple consists of + (Sensor Name (string), Sensor Value (string), Sensor Unit (string) ) """ + sensor = obd_sensors.SENSORS[sensor_index] + r = self.get_sensor_value(sensor) + return (sensor.name,r, sensor.unit) + + def sensor_names(self): + """Internal use only: not a public interface""" + names = [] + for s in obd_sensors.SENSORS: + names.append(s.name) + return names + + def get_tests_MIL(self): + statusText=["Unsupported","Supported - Completed","Unsupported","Supported - Incompleted"] + + statusRes = self.sensor(1)[1] #GET values + statusTrans = [] #translate values to text + + statusTrans.append(str(statusRes[0])) #DTCs + + if statusRes[1]==0: #MIL + statusTrans.append("Off") + else: + statusTrans.append("On") + + for i in range(2,len(statusRes)): #Tests + statusTrans.append(statusText[statusRes[i]]) + + return statusTrans + + # + # fixme: j1979 specifies that the program should poll until the number + # of returned DTCs matches the number indicated by a call to PID 01 + # + def get_dtc(self): + """Returns a list of all pending DTC codes. Each element consists of + a 2-tuple: (DTC code (string), Code description (string) )""" + dtcLetters = ["P", "C", "B", "U"] + r = self.sensor(1)[1] #data + dtcNumber = r[0] + mil = r[1] + DTCCodes = [] + + + print "Number of stored DTC:" + str(dtcNumber) + " MIL: " + str(mil) + # get all DTC, 3 per mesg response + for i in range(0, ((dtcNumber+2)/3)): + self.send_command(GET_DTC_COMMAND) + res = self.get_result() + print "DTC result:" + res + for i in range(0, 3): + val1 = hex_to_int(res[3+i*6:5+i*6]) + val2 = hex_to_int(res[6+i*6:8+i*6]) #get DTC codes from response (3 DTC each 2 bytes) + val = (val1<<8)+val2 #DTC val as int + + if val==0: #skip fill of last packet + break + + DTCStr=dtcLetters[(val&0xC000)>14]+str((val&0x3000)>>12)+str((val&0x0f00)>>8)+str((val&0x00f0)>>4)+str(val&0x000f) + DTCCodes.append(["Active",DTCStr]) + + #read mode 7 + self.send_command(GET_FREEZE_DTC_COMMAND) + res = self.get_result() + + if res[:7] == "NODATA": #no freeze frame + return DTCCodes + + print "DTC freeze result:" + res + for i in range(0, 3): + val1 = hex_to_int(res[3+i*6:5+i*6]) + val2 = hex_to_int(res[6+i*6:8+i*6]) #get DTC codes from response (3 DTC each 2 bytes) + val = (val1<<8)+val2 #DTC val as int + + if val==0: #skip fill of last packet + break + + DTCStr=dtcLetters[(val&0xC000)>14]+str((val&0x3000)>>12)+str((val&0x0f00)>>8)+str((val&0x00f0)>>4)+str(val&0x000f) + DTCCodes.append(["Passive",DTCStr]) + + return DTCCodes + + def clear_dtc(self): + """Clears all DTCs and freeze frame data""" + self.send_command(CLEAR_DTC_COMMAND) + r = self.get_result() + return r + + def log(self, sensor_index, filename): + file = open(filename, "w") + start_time = time.time() + if file: + data = self.sensor(sensor_index) + file.write("%s \t%s(%s)\n" % \ + ("Time", string.strip(data[0]), data[2])) + while 1: + now = time.time() + data = self.sensor(sensor_index) + line = "%.6f,\t%s\n" % (now - start_time, data[1]) + file.write(line) + file.flush() diff --git a/pyobd/obd_utils.py b/pyobd/obd_utils.py index b49b4b62..b636e0da 100644 --- a/pyobd/obd_utils.py +++ b/pyobd/obd_utils.py @@ -11,7 +11,7 @@ def tryPort(portStr): except serial.SerialException: pass - except OSError, e: + except OSError as e: if e.errno != errno.ENOENT: # permit "no such file or directory" errors raise e From 00768c10ecfb19a216f54c10007892c477a3e191 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 01:31:13 -0400 Subject: [PATCH 015/569] fixed error reporting --- pyobd/{obd_capture.py => obd.py} | 11 +++++++---- pyobd/obd_io.py | 22 +++++++++++++--------- 2 files changed, 20 insertions(+), 13 deletions(-) rename pyobd/{obd_capture.py => obd.py} (93%) diff --git a/pyobd/obd_capture.py b/pyobd/obd.py similarity index 93% rename from pyobd/obd_capture.py rename to pyobd/obd.py index 8daf6cfb..11f9603b 100644 --- a/pyobd/obd_capture.py +++ b/pyobd/obd.py @@ -10,7 +10,10 @@ from obd_utils import scanSerial -class OBD_Capture(): + + +class OBD(): + """ class representing an OBD-II connection """ def __init__(self): self.supportedSensorList = [] @@ -40,7 +43,7 @@ def is_connected(self): return (self.port is not None) and (self.port.state == State.Connected) def get_port_name(self): - return self.port.port.name + return self.port.get_port_name() def getSupportedSensorList(self): return self.supportedSensorList @@ -48,7 +51,7 @@ def getSupportedSensorList(self): def capture_data(self): text = "" - #Find supported sensors - by getting PIDs from OBD + # Find supported sensors - by getting PIDs from OBD # its a string of binary 01010101010101 # 1 means the sensor is supported self.supp = self.port.sensor(0)[1] @@ -87,7 +90,7 @@ def capture_data(self): if __name__ == "__main__": - o = OBD_Capture() + o = OBD() o.connect() time.sleep(3) if not o.is_connected(): diff --git a/pyobd/obd_io.py b/pyobd/obd_io.py index 7e09e774..26c009d5 100644 --- a/pyobd/obd_io.py +++ b/pyobd/obd_io.py @@ -68,23 +68,23 @@ def __init__(self, portname): timeout = timeout) except serial.SerialException as e: - error(e) - return None + self.error(e) + return - print "Interface successfully " + self.port.portstr + " opened" + print "Interface successfully opened on " + self.get_port_name() print "Connecting to ECU..." try: self.send_command("atz") # initialize time.sleep(1) except serial.SerialException as e: - error(e) - return None + self.error(e) + return self.ELMver = self.get_result() if self.ELMver is None : - error("ELMver returned None") - return None + self.error("ELMver returned None") + return print "atz response:" + self.ELMver self.send_command("ate0") # echo off @@ -94,10 +94,10 @@ def __init__(self, portname): if ready is None: self.state = State.Unconnected - return None + return print "0100 response:" + ready - return None + def error(self, msg=None): """ called when connection error has been encountered """ @@ -108,6 +108,10 @@ def error(self, msg=None): self.state = State.Unconnected + def get_port_name(self): + return self.port.portstr if (self.port is not None) else "No Port" + + def close(self): """ Resets device and closes all associated filehandles""" From bd6576be339cd72cc0f70dce381afd277116dbd7 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 01:49:53 -0400 Subject: [PATCH 016/569] simplified sensors sensing --- pyobd/obd.py | 54 ++++++++++++++++---------------------------- pyobd/obd_io.py | 1 + pyobd/obd_sensors.py | 12 ++++++---- 3 files changed, 27 insertions(+), 40 deletions(-) diff --git a/pyobd/obd.py b/pyobd/obd.py index 11f9603b..243f2b4a 100644 --- a/pyobd/obd.py +++ b/pyobd/obd.py @@ -5,10 +5,10 @@ import serial import platform import obd_sensors -from datetime import datetime import time from obd_utils import scanSerial +from obd_sensors import sensors @@ -16,9 +16,11 @@ class OBD(): """ class representing an OBD-II connection """ def __init__(self): - self.supportedSensorList = [] self.port = None - localtime = time.localtime(time.time()) + self.sensors = [] + self.supportedSensors = [] + self.unsupportedSensors = [] + def connect(self, portstr=None): """ attempts to instantiate an OBDPort object. Return boolean for success/failure""" @@ -48,44 +50,26 @@ def get_port_name(self): def getSupportedSensorList(self): return self.supportedSensorList - def capture_data(self): - text = "" - # Find supported sensors - by getting PIDs from OBD + def load_sensors(self): + + self.sensors = [] + self.supportedSensors = [] + self.unsupportedSensors = [] + + # Find supported sensors - by getting PIDs from OBD (sensor zero) # its a string of binary 01010101010101 # 1 means the sensor is supported - self.supp = self.port.sensor(0)[1] - self.supportedSensorList = [] - self.unsupportedSensorList = [] + supported = self.port.get_sensor_value(sensors[0]) # loop through PIDs binary - for i in range(0, len(self.supp)): - if self.supp[i] == "1": - # store index of sensor and sensor object - self.supportedSensorList.append([i+1, obd_sensors.SENSORS[i+1]]) + for i in range(len(supported)): + if supported[i] == "1": + self.supportedSensors.append(i) + self.sensors.append(sensors[i]) else: - self.unsupportedSensorList.append([i+1, obd_sensors.SENSORS[i+1]]) - - for supportedSensor in self.supportedSensorList: - text += "supported sensor index = " + str(supportedSensor[0]) + " " + str(supportedSensor[1].shortname) + "\n" - - time.sleep(3) - - if(self.port is None): - return None - - #Loop until Ctrl C is pressed - localtime = datetime.now() - current_time = str(localtime.hour)+":"+str(localtime.minute)+":"+str(localtime.second)+"."+str(localtime.microsecond) - #log_string = current_time + "\n" - text = current_time + "\n" - results = {} - for supportedSensor in self.supportedSensorList: - sensorIndex = supportedSensor[0] - (name, value, unit) = self.port.sensor(sensorIndex) - text += name + " = " + str(value) + " " + str(unit) + "\n" - - return text + self.unsupportedSensors.append(i) + if __name__ == "__main__": diff --git a/pyobd/obd_io.py b/pyobd/obd_io.py index 26c009d5..aed35301 100644 --- a/pyobd/obd_io.py +++ b/pyobd/obd_io.py @@ -215,6 +215,7 @@ def sensor(self , sensor_index): r = self.get_sensor_value(sensor) return (sensor.name,r, sensor.unit) + def sensor_names(self): """Internal use only: not a public interface""" names = [] diff --git a/pyobd/obd_sensors.py b/pyobd/obd_sensors.py index ee18f10f..764d5afd 100644 --- a/pyobd/obd_sensors.py +++ b/pyobd/obd_sensors.py @@ -128,15 +128,17 @@ def hex_to_bitstring(str): bitstring += '0' return bitstring + class Sensor: def __init__(self, shortName, sensorName, sensorcommand, sensorValueFunction, u): self.shortname = shortName - self.name = sensorName - self.cmd = sensorcommand - self.value= sensorValueFunction - self.unit = u + self.name = sensorName + self.cmd = sensorcommand + self.value = sensorValueFunction + self.unit = u + -SENSORS = [ +sensors = [ Sensor("pids" , "Supported PIDs" , "0100" , hex_to_bitstring ,"" ), Sensor("dtc_status" , "S-S DTC Cleared" , "0101" , dtc_decrypt ,"" ), Sensor("dtc_ff" , "DTC C-F-F" , "0102" , cpass ,"" ), From 2b3a1698612ddbe60c62371c2cfb4891cb001c19 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 02:01:40 -0400 Subject: [PATCH 017/569] successful test of sensor loading --- pyobd/obd.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyobd/obd.py b/pyobd/obd.py index 243f2b4a..2f0aa530 100644 --- a/pyobd/obd.py +++ b/pyobd/obd.py @@ -52,7 +52,8 @@ def getSupportedSensorList(self): def load_sensors(self): - + """ queries for available sensors, and compiles lists of indices and sensor objects """ + self.sensors = [] self.supportedSensors = [] self.unsupportedSensors = [] @@ -71,6 +72,11 @@ def load_sensors(self): self.unsupportedSensors.append(i) + def printSensors(self): + for sensor in self.sensors: + print sensor.name + + if __name__ == "__main__": @@ -81,4 +87,5 @@ def load_sensors(self): print "Not connected" else: print "Connected to " + o.get_port_name() - o.capture_data() + o.load_sensors() + o.printSensors() From 95daa5801462766ec41fadf17cb00b8bf65804ef Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 02:21:53 -0400 Subject: [PATCH 018/569] remove old functions --- pyobd/obd.py | 5 ++++- pyobd/obd_io.py | 23 ++++------------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/pyobd/obd.py b/pyobd/obd.py index 2f0aa530..9077d609 100644 --- a/pyobd/obd.py +++ b/pyobd/obd.py @@ -13,7 +13,7 @@ class OBD(): - """ class representing an OBD-II connection """ + """ class representing an OBD-II connection, with it's assorted sensors """ def __init__(self): self.port = None @@ -76,6 +76,9 @@ def printSensors(self): for sensor in self.sensors: print sensor.name + def valueOf(sensor): + return self.port.get_sensor_value(sensor) + if __name__ == "__main__": diff --git a/pyobd/obd_io.py b/pyobd/obd_io.py index aed35301..e1baa27e 100644 --- a/pyobd/obd_io.py +++ b/pyobd/obd_io.py @@ -30,8 +30,8 @@ from obd_utils import hex_to_int -GET_DTC_COMMAND = "03" -CLEAR_DTC_COMMAND = "04" +GET_DTC_COMMAND = "03" +CLEAR_DTC_COMMAND = "04" GET_FREEZE_DTC_COMMAND = "07" @@ -188,11 +188,11 @@ def get_result(self): return None return buffer else: - print "NO self.port!" + print "No port!" return None # get sensor value from command - def get_sensor_value(self,sensor): + def get_sensor_value(self, sensor): """Internal use only: not a public interface""" cmd = sensor.cmd self.send_command(cmd) @@ -207,21 +207,6 @@ def get_sensor_value(self,sensor): return data - # return string of sensor name and value from sensor index - def sensor(self , sensor_index): - """Returns 3-tuple of given sensors. 3-tuple consists of - (Sensor Name (string), Sensor Value (string), Sensor Unit (string) ) """ - sensor = obd_sensors.SENSORS[sensor_index] - r = self.get_sensor_value(sensor) - return (sensor.name,r, sensor.unit) - - - def sensor_names(self): - """Internal use only: not a public interface""" - names = [] - for s in obd_sensors.SENSORS: - names.append(s.name) - return names def get_tests_MIL(self): statusText=["Unsupported","Supported - Completed","Unsupported","Supported - Incompleted"] From 193108ce92f0a3c0779c9a59deca83d69fae502b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 02:49:50 -0400 Subject: [PATCH 019/569] minor error catching --- pyobd/__init__.py | 4 ++++ pyobd/obd.py | 28 ++++++++++++++++------------ pyobd/obd_io.py | 9 ++++++++- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/pyobd/__init__.py b/pyobd/__init__.py index e69de29b..541524d9 100644 --- a/pyobd/__init__.py +++ b/pyobd/__init__.py @@ -0,0 +1,4 @@ + +from obd import OBD +from obd_sensors import sensors +from obd_utils import scanSerial diff --git a/pyobd/obd.py b/pyobd/obd.py index 9077d609..9406aed5 100644 --- a/pyobd/obd.py +++ b/pyobd/obd.py @@ -1,33 +1,33 @@ #!/usr/bin/env python -from obd_io import OBDPort -from obd_io import State -import serial -import platform -import obd_sensors import time -from obd_utils import scanSerial +from obd_io import State +from obd_io import OBDPort from obd_sensors import sensors +from obd_utils import scanSerial class OBD(): - """ class representing an OBD-II connection, with it's assorted sensors """ + """ class representing an OBD-II connection with it's assorted sensors """ - def __init__(self): + def __init__(self, portstr=None): self.port = None self.sensors = [] self.supportedSensors = [] self.unsupportedSensors = [] + # initialize by connecting and loading sensors + if self.connect(portstr): + self.load_sensors() + def connect(self, portstr=None): """ attempts to instantiate an OBDPort object. Return boolean for success/failure""" if portstr is None: portnames = scanSerial() - print portnames for port in portnames: @@ -40,12 +40,15 @@ def connect(self, portstr=None): self.port = OBDPort(portstr) return self.is_connected() - + + def is_connected(self): return (self.port is not None) and (self.port.state == State.Connected) + def get_port_name(self): return self.port.get_port_name() + def getSupportedSensorList(self): return self.supportedSensorList @@ -76,6 +79,7 @@ def printSensors(self): for sensor in self.sensors: print sensor.name + def valueOf(sensor): return self.port.get_sensor_value(sensor) @@ -84,11 +88,11 @@ def valueOf(sensor): if __name__ == "__main__": o = OBD() - o.connect() + #o.connect() time.sleep(3) if not o.is_connected(): print "Not connected" else: print "Connected to " + o.get_port_name() - o.load_sensors() + #o.load_sensors() o.printSensors() diff --git a/pyobd/obd_io.py b/pyobd/obd_io.py index e1baa27e..e75d6f9a 100644 --- a/pyobd/obd_io.py +++ b/pyobd/obd_io.py @@ -70,6 +70,9 @@ def __init__(self, portname): except serial.SerialException as e: self.error(e) return + except OSError as e: + self.error(e) + return print "Interface successfully opened on " + self.get_port_name() print "Connecting to ECU..." @@ -102,9 +105,13 @@ def __init__(self, portname): def error(self, msg=None): """ called when connection error has been encountered """ print "Connection Error:" + if msg is not None: print msg - self.port.close() + + if self.port is not None: + self.port.close() + self.state = State.Unconnected From f91b53d775c5a84341cdd8061bd3cc66d1a150ee Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 12:56:10 -0400 Subject: [PATCH 020/569] started bringing the readme up to speed --- README.md | 103 +++++++++---------------------------------- pyobd/obd.py | 6 +-- pyobd/obd_io.py | 17 +------ pyobd/obd_sensors.py | 3 ++ 4 files changed, 28 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 318bcf1e..b5035aae 100644 --- a/README.md +++ b/README.md @@ -1,102 +1,43 @@ -pyobd -===== +pyOBD-IO +======== -##### OBD-Pi: Raspberry Pi Displaying Car Diagnostics (OBD-II) Data On An Aftermarket Head Unit +##### A python module for handling realtime sensor data from OBD-II vehicle ports -In this tutorial you will learn how to connect your Raspberry Pi to a Bluetooth OBD-II adapter and display realtime engine data to your cars aftermarket head unit. +This library is forked from: -Hardware Required: ++ https://github.com/peterh/pyobd ++ https://github.com/Pbartek/pyobd-pi -1. Raspberry Pi -2. Aftermarket head unit (Note: Must support Auxiliary input) -3. Plugable USB Bluetooth 4.0 Low Energy Micro Adapter -4. 2A Car Supply / Switch or Micro USB Car Charger -5. ELM327 Bluetooth Adapter or ELM327 USB Cable -6. RCA cable -7. Keyboard (*optional) -What is OBD-II? -OBD stands for On-Board Diagnostics, and this standard connector has been mandated in the US since 1996. Now you can think of OBD-II as an on-board computer system that is responsible for monitoring your vehicle’s engine, transmission, and emissions control components. +### Dependencies -Vehicles that comply with the OBD-II standards will have a data connector within about 2 feet of the steering wheel. The OBD connector is officially called a SAE J1962 Diagnostic Connector, but is also known by DLC, OBD Port, or OBD connector. It has positions for 16 pins. ++ pySerial ++ OBD-II addapter (ELM327 Bluetooth Adapter or ELM327 USB Cable) -pyOBD? -pyOBD (aka pyOBD-II or pyOBD2) is an open source OBD-II (SAE-J1979) compliant scantool software written entirely in Python. It is designed to interface with low-cost ELM 32x OBD-II diagnostic interfaces such as ELM-USB. It will basically allow you to talk to your car's ECU, display fault codes, display measured values, read status tests, etc. -I took a fork of pyOBD’s software from their GitHub repository, https://github.com/peterh/pyobd, and used this as the basis for my program. +### Usage -The program will connect through the OBD-II interface, display the gauges available dependent on the particular vehicle and display realtime engine data to the cars aftermarket head unit in an interactive GUI. -Software Installation -Before you start you will need a working install of Raspbian with network access. +After installing the library, simply import pyobd, and create a new OBD connection object: -We'll be doing this from a console cable connection, but you can just as easily do it from the direct HDMI/TV console or by SSH'ing in. Whatever gets you to a shell will work! + import pyobd + connection = pyobd.OBD() # create connection object -Note: For the following command line instructions, do not type the '\#', that is only to indicate that it is a command to enter. +By default, pyOBD-IO will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. -Before proceeding, run: + import pyobd + connection = pyobd.OBD("/dev/ttyUSB0") # create connection with USB 0 - # sudo apt-get update - # sudo apt-get upgrade - # sudo apt-get autoremove - # sudo reboot +You can also use the scanSerial helper retrieve a list of connected ports -Install these components using the command: + import pyobd - # sudo apt-get install python-serial - # sudo apt-get install bluetooth bluez-utils blueman - # sudo apt-get install python-wxgtk2.8 python-wxtools wx2.8-i18n libwxgtk2.8-dev - # sudo apt-get install git-core - # sudo reboot + ports = pyobd.scanSerial() + print ports + connection = pyobd.OBD(ports[0]) # connect to the first port in the list -Next, download the OBD-Pi Software direct from GitHub (https://github.com/Pbartek/pyobd-pi.git) +Once a connection is made, pyOBD-IO will load a list of the available sensors in your car. A "Sensor" in pyOBD is an object containing its name, units, and retrival information. -Or using the command: - # cd ~ - # git clone https://github.com/Pbartek/pyobd-pi.git -Vehicle Installation -The vehicle installation is quite simple. - -1. Insert the USB Bluetooth dongle into the Raspberry Pi along with the SD card. - -2. Insert the OBD-II Bluetooth adapter into the SAE J196216 (OBD Port) connector. - -3. Connect you RCA cable to the back of your aftermarket head unit and plug the other end into your Raspberry Pi. - -4. Install your 2A Car Supply / Switch or Micro USB Car Charger. - -5. Finally turn your key to the ON position and navigate your head unit to Auxiliary input. - -6. Enter your login credentials and run: - - # startx - -7. Launch BlueZ, the Bluetooth stack for Linux. Pair + Trust your ELM327 Bluetooth Adapter and Connect To: SPP Dev. You should see the Notification "Serial port connected to /dev/rfcomm0" - -Note: Click the Bluetooth icon, bottom right (Desktop) to configure your device. Right click on your Bluetooth device to bring up Connect To: SPP Dev. - -8. Open up Terminal and run: - - # cd pyobd-pi - # sudo su - # python obd_gui.py - -Use the Left and Right arrow key to cycle through the gauge display. -Note: Left and Right mouse click will also work - -To exit the program just press Control and C or Alt and Esc. - -Update: - -Data Logging - -If you would like to log your data run: - - # cd pyobd-pi - # python obd_recorder.py - -The logged data file will be saved under: -/home/username/pyobd-pi/log/ Enjoy and drive safe! diff --git a/pyobd/obd.py b/pyobd/obd.py index 9406aed5..d75beb61 100644 --- a/pyobd/obd.py +++ b/pyobd/obd.py @@ -49,10 +49,6 @@ def is_connected(self): def get_port_name(self): return self.port.get_port_name() - - def getSupportedSensorList(self): - return self.supportedSensorList - def load_sensors(self): """ queries for available sensors, and compiles lists of indices and sensor objects """ @@ -77,7 +73,7 @@ def load_sensors(self): def printSensors(self): for sensor in self.sensors: - print sensor.name + print str(sensor) def valueOf(sensor): diff --git a/pyobd/obd_io.py b/pyobd/obd_io.py index e75d6f9a..b9b6a411 100644 --- a/pyobd/obd_io.py +++ b/pyobd/obd_io.py @@ -214,7 +214,7 @@ def get_sensor_value(self, sensor): return data - + ''' def get_tests_MIL(self): statusText=["Unsupported","Supported - Completed","Unsupported","Supported - Incompleted"] @@ -232,6 +232,7 @@ def get_tests_MIL(self): statusTrans.append(statusText[statusRes[i]]) return statusTrans + ''' # # fixme: j1979 specifies that the program should poll until the number @@ -290,17 +291,3 @@ def clear_dtc(self): self.send_command(CLEAR_DTC_COMMAND) r = self.get_result() return r - - def log(self, sensor_index, filename): - file = open(filename, "w") - start_time = time.time() - if file: - data = self.sensor(sensor_index) - file.write("%s \t%s(%s)\n" % \ - ("Time", string.strip(data[0]), data[2])) - while 1: - now = time.time() - data = self.sensor(sensor_index) - line = "%.6f,\t%s\n" % (now - start_time, data[1]) - file.write(line) - file.flush() diff --git a/pyobd/obd_sensors.py b/pyobd/obd_sensors.py index 764d5afd..d407c914 100644 --- a/pyobd/obd_sensors.py +++ b/pyobd/obd_sensors.py @@ -137,6 +137,9 @@ def __init__(self, shortName, sensorName, sensorcommand, sensorValueFunction, u) self.value = sensorValueFunction self.unit = u + def __str__(self): + return self.name + sensors = [ Sensor("pids" , "Supported PIDs" , "0100" , hex_to_bitstring ,"" ), From f197af209557d108f9a67c58bf396245b8bdaf3c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 13:07:25 -0400 Subject: [PATCH 021/569] made filenames consitent --- pyobd/obd.py | 4 ++-- pyobd/{obd2_codes.py => obd_codes.py} | 0 pyobd/{obd_io.py => obd_port.py} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename pyobd/{obd2_codes.py => obd_codes.py} (100%) rename pyobd/{obd_io.py => obd_port.py} (100%) diff --git a/pyobd/obd.py b/pyobd/obd.py index d75beb61..53470dae 100644 --- a/pyobd/obd.py +++ b/pyobd/obd.py @@ -2,8 +2,8 @@ import time -from obd_io import State -from obd_io import OBDPort +from obd_port import State +from obd_port import OBDPort from obd_sensors import sensors from obd_utils import scanSerial diff --git a/pyobd/obd2_codes.py b/pyobd/obd_codes.py similarity index 100% rename from pyobd/obd2_codes.py rename to pyobd/obd_codes.py diff --git a/pyobd/obd_io.py b/pyobd/obd_port.py similarity index 100% rename from pyobd/obd_io.py rename to pyobd/obd_port.py From 6cad0efc9cbf38021bd2d6d14aed76e6a9e75167 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 15:34:35 -0400 Subject: [PATCH 022/569] allowed sensors to be referenced by name --- pyobd/obd.py | 20 +++++----- pyobd/obd_sensors.py | 92 ++++++++++++++++++++++++-------------------- 2 files changed, 60 insertions(+), 52 deletions(-) diff --git a/pyobd/obd.py b/pyobd/obd.py index 53470dae..49f3274f 100644 --- a/pyobd/obd.py +++ b/pyobd/obd.py @@ -15,8 +15,6 @@ class OBD(): def __init__(self, portstr=None): self.port = None self.sensors = [] - self.supportedSensors = [] - self.unsupportedSensors = [] # initialize by connecting and loading sensors if self.connect(portstr): @@ -54,30 +52,32 @@ def load_sensors(self): """ queries for available sensors, and compiles lists of indices and sensor objects """ self.sensors = [] - self.supportedSensors = [] - self.unsupportedSensors = [] # Find supported sensors - by getting PIDs from OBD (sensor zero) # its a string of binary 01010101010101 # 1 means the sensor is supported - supported = self.port.get_sensor_value(sensors[0]) + supported = self.valueOf(sensors.PIDS) # loop through PIDs binary for i in range(len(supported)): if supported[i] == "1": - self.supportedSensors.append(i) - self.sensors.append(sensors[i]) - else: - self.unsupportedSensors.append(i) + sensor = sensors.by_PID[i] + sensor.supported = True + self.supportedSensors.append(sensor) def printSensors(self): for sensor in self.sensors: print str(sensor) + def hasSensor(sensor): + return sensor.supported def valueOf(sensor): - return self.port.get_sensor_value(sensor) + if self.hasSensor(sensor): + return self.port.get_sensor_value(sensor) + else: + return "Unsupported Sensor" diff --git a/pyobd/obd_sensors.py b/pyobd/obd_sensors.py index d407c914..36530dd3 100644 --- a/pyobd/obd_sensors.py +++ b/pyobd/obd_sensors.py @@ -130,54 +130,62 @@ def hex_to_bitstring(str): class Sensor: - def __init__(self, shortName, sensorName, sensorcommand, sensorValueFunction, u): - self.shortname = shortName - self.name = sensorName - self.cmd = sensorcommand - self.value = sensorValueFunction - self.unit = u + def __init__(self, shortname, name, command, valueFunction, unit): + self.shortname = shortname + self.name = name + self.cmd = command + self.value = valueFunction + self.unit = unit + self.supported = False def __str__(self): return self.name -sensors = [ - Sensor("pids" , "Supported PIDs" , "0100" , hex_to_bitstring ,"" ), - Sensor("dtc_status" , "S-S DTC Cleared" , "0101" , dtc_decrypt ,"" ), - Sensor("dtc_ff" , "DTC C-F-F" , "0102" , cpass ,"" ), - Sensor("fuel_status" , "Fuel System Stat" , "0103" , cpass ,"" ), - Sensor("load" , "Calc Load Value" , "01041", percent_scale ,"" ), - Sensor("temp" , "Coolant Temp" , "0105" , temp ,"F" ), - Sensor("short_term_fuel_trim_1", "S-T Fuel Trim" , "0106" , fuel_trim_percent,"%" ), - Sensor("long_term_fuel_trim_1" , "L-T Fuel Trim" , "0107" , fuel_trim_percent,"%" ), - Sensor("short_term_fuel_trim_2", "S-T Fuel Trim" , "0108" , fuel_trim_percent,"%" ), - Sensor("long_term_fuel_trim_2" , "L-T Fuel Trim" , "0109" , fuel_trim_percent,"%" ), - Sensor("fuel_pressure" , "FuelRail Pressure" , "010A" , cpass ,"" ), - Sensor("manifold_pressure" , "Intk Manifold" , "010B" , intake_m_pres ,"psi" ), - Sensor("rpm" , "Engine RPM" , "010C1", rpm ,"" ), - Sensor("speed" , "Vehicle Speed" , "010D1", speed ,"MPH" ), - Sensor("timing_advance" , "Timing Advance" , "010E" , timing_advance ,"degrees"), - Sensor("intake_air_temp" , "Intake Air Temp" , "010F" , temp ,"F" ), - Sensor("maf" , "AirFlow Rate(MAF)" , "0110" , maf ,"lb/min" ), - Sensor("throttle_pos" , "Throttle Position" , "01111", throttle_pos ,"%" ), - Sensor("secondary_air_status" , "2nd Air Status" , "0112" , cpass ,"" ), - Sensor("o2_sensor_positions" , "Loc of O2 sensors" , "0113" , cpass ,"" ), - Sensor("o211" , "O2 Sensor: 1 - 1" , "0114" , fuel_trim_percent,"%" ), - Sensor("o212" , "O2 Sensor: 1 - 2" , "0115" , fuel_trim_percent,"%" ), - Sensor("o213" , "O2 Sensor: 1 - 3" , "0116" , fuel_trim_percent,"%" ), - Sensor("o214" , "O2 Sensor: 1 - 4" , "0117" , fuel_trim_percent,"%" ), - Sensor("o221" , "O2 Sensor: 2 - 1" , "0118" , fuel_trim_percent,"%" ), - Sensor("o222" , "O2 Sensor: 2 - 2" , "0119" , fuel_trim_percent,"%" ), - Sensor("o223" , "O2 Sensor: 2 - 3" , "011A" , fuel_trim_percent,"%" ), - Sensor("o224" , "O2 Sensor: 2 - 4" , "011B" , fuel_trim_percent,"%" ), - Sensor("obd_standard" , "OBD Designation" , "011C" , cpass ,"" ), - Sensor("o2_sensor_position_b" , "Loc of O2 sensor" , "011D" , cpass ,"" ), - Sensor("aux_input" , "Aux input status" , "011E" , cpass ,"" ), - Sensor("engine_time" , "Engine Start MIN" , "011F" , sec_to_min ,"min" ), - Sensor("engine_mil_time" , "Engine Run MIL" , "014D" , sec_to_min ,"min" ), +class sensors(): + + # sensors must be in order of PID + # note, the SHORTNAME field will be used as the dict key for that sensor + by_PID = [ + Sensor("PIDS" , "Supported PIDs" , "0100" , hex_to_bitstring , "" ), + Sensor("DTC_STATUS" , "S-S DTC Cleared" , "0101" , dtc_decrypt , "" ), + Sensor("DTC_FF" , "DTC C-F-F" , "0102" , cpass , "" ), + Sensor("FUEL_STATUS" , "Fuel System Status" , "0103" , cpass , "" ), + Sensor("LOAD" , "Calculated Engine Load" , "01041", percent_scale , "%" ), + Sensor("COOLANT_TEMP" , "Engine Coolant Temperature" , "0105" , temp , "F" ), + Sensor("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "0106" , fuel_trim_percent, "%" ), + Sensor("Long_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "0107" , fuel_trim_percent, "%" ), + Sensor("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "0108" , fuel_trim_percent, "%" ), + Sensor("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "0109" , fuel_trim_percent, "%" ), + Sensor("FUEL_PRESSURE" , "Fuel Pressure" , "010A" , cpass , "" ), + Sensor("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "010B" , intake_m_pres , "psi" ), + Sensor("RPM" , "Engine RPM" , "010C1", rpm , "" ), + Sensor("SPEED" , "Vehicle Speed" , "010D1", speed , "MPH" ), + Sensor("TIMING_ADVANCE" , "Timing Advance" , "010E" , timing_advance , "degrees"), + Sensor("INTAKE_TEMP" , "Intake Air Temp" , "010F" , temp , "F" ), + Sensor("MAF" , "Air Flow Rate (MAF)" , "0110" , maf , "lb/min" ), + Sensor("THROTTLE" , "Throttle Position" , "01111", throttle_pos , "%" ), + Sensor("AIR_STATUS" , "Secondary Air Status" , "0112" , cpass , "" ), + Sensor("O2_SENSORS" , "O2 Sensors Present" , "0113" , cpass , "" ), + Sensor("O2_B1_S1" , "O2: Bank 1 - Sensor 1" , "0114" , fuel_trim_percent, "%" ), + Sensor("O2_B1_S2" , "O2: Bank 1 - Sensor 2" , "0115" , fuel_trim_percent, "%" ), + Sensor("O2_B1_S3" , "O2: Bank 1 - Sensor 3" , "0116" , fuel_trim_percent, "%" ), + Sensor("O2_B1_S4" , "O2: Bank 1 - Sensor 4" , "0117" , fuel_trim_percent, "%" ), + Sensor("O2_B2_S1" , "O2: Bank 2 - Sensor 1" , "0118" , fuel_trim_percent, "%" ), + Sensor("O2_B2_S2" , "O2: Bank 2 - Sensor 2" , "0119" , fuel_trim_percent, "%" ), + Sensor("O2_B2_S3" , "O2: Bank 2 - Sensor 3" , "011A" , fuel_trim_percent, "%" ), + Sensor("O2_B2_S4" , "O2: Bank 2 - Sensor 4" , "011B" , fuel_trim_percent, "%" ), + Sensor("OBD_STANDARDS" , "OBD Standards Compliance" , "011C" , cpass , "" ), + Sensor("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "011D" , cpass , "" ), + Sensor("AUX_INPUT_STATUS" , "Auxiliary input status" , "011E" , cpass , "" ), + Sensor("RUN_TIME" , "Engine Start MIN" , "011F" , sec_to_min , "min" ), + #Sensor("RUN_TIME_MIL" , "Engine Run MIL" , "014D" , sec_to_min , "min" ), ] - - + +# assemble the dict, to access sensors by name +for sensor in sensors.by_PID: + sensors.__dict__[sensor.shortname] = sensor + #___________________________________________________________ def test(): From b9f348c5e102e88114e81460b66f3b5f9fa5a569 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 15:47:28 -0400 Subject: [PATCH 023/569] added fuel_pressure function --- pyobd/obd_sensors.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyobd/obd_sensors.py b/pyobd/obd_sensors.py index 36530dd3..e7632ca4 100644 --- a/pyobd/obd_sensors.py +++ b/pyobd/obd_sensors.py @@ -34,7 +34,11 @@ def throttle_pos(code): code = hex_to_int(code) return code * 100.0 / 255.0 -def intake_m_pres(code): # in kPa +def fuel_pressure(code): # in kPa + code = hex_to_int(code) + return (code * 3) / 0.14504 + +def intake_pressure(code): # in kPa code = hex_to_int(code) return code / 0.14504 @@ -157,8 +161,8 @@ class sensors(): Sensor("Long_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "0107" , fuel_trim_percent, "%" ), Sensor("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "0108" , fuel_trim_percent, "%" ), Sensor("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "0109" , fuel_trim_percent, "%" ), - Sensor("FUEL_PRESSURE" , "Fuel Pressure" , "010A" , cpass , "" ), - Sensor("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "010B" , intake_m_pres , "psi" ), + Sensor("FUEL_PRESSURE" , "Fuel Pressure" , "010A" , fuel_pressure , "psi" ), + Sensor("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "010B" , intake_pressure , "psi" ), Sensor("RPM" , "Engine RPM" , "010C1", rpm , "" ), Sensor("SPEED" , "Vehicle Speed" , "010D1", speed , "MPH" ), Sensor("TIMING_ADVANCE" , "Timing Advance" , "010E" , timing_advance , "degrees"), From 7e7f70e9b71416ca66ee68045450145ab74e6a74 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 20:01:57 -0400 Subject: [PATCH 024/569] updated ending condition for load sensor loop --- pyobd/obd.py | 112 +++++++++++++++++++++++---------------------- pyobd/obd_utils.py | 3 +- 2 files changed, 58 insertions(+), 57 deletions(-) diff --git a/pyobd/obd.py b/pyobd/obd.py index 49f3274f..ee5831b7 100644 --- a/pyobd/obd.py +++ b/pyobd/obd.py @@ -10,85 +10,87 @@ class OBD(): - """ class representing an OBD-II connection with it's assorted sensors """ + """ class representing an OBD-II connection with it's assorted sensors """ - def __init__(self, portstr=None): - self.port = None - self.sensors = [] + def __init__(self, portstr=None): + self.port = None + self.sensors = [] - # initialize by connecting and loading sensors - if self.connect(portstr): - self.load_sensors() + # initialize by connecting and loading sensors + if self.connect(portstr): + self.load_sensors() - def connect(self, portstr=None): - """ attempts to instantiate an OBDPort object. Return boolean for success/failure""" + def connect(self, portstr=None): + """ attempts to instantiate an OBDPort object. Return boolean for success/failure""" - if portstr is None: - portnames = scanSerial() + if portstr is None: + portnames = scanSerial() - for port in portnames: + for port in portnames: - self.port = OBDPort(port) + self.port = OBDPort(port) - if(self.port.state == State.Connected): - # success! stop searching for serial - break - else: - self.port = OBDPort(portstr) + if(self.port.state == State.Connected): + # success! stop searching for serial + break + else: + self.port = OBDPort(portstr) - return self.is_connected() + return self.is_connected() - def is_connected(self): - return (self.port is not None) and (self.port.state == State.Connected) + def is_connected(self): + return (self.port is not None) and (self.port.state == State.Connected) - def get_port_name(self): - return self.port.get_port_name() + def get_port_name(self): + return self.port.get_port_name() - def load_sensors(self): - """ queries for available sensors, and compiles lists of indices and sensor objects """ + def load_sensors(self): + """ queries for available sensors, and compiles lists of indices and sensor objects """ - self.sensors = [] + self.sensors = [] - # Find supported sensors - by getting PIDs from OBD (sensor zero) - # its a string of binary 01010101010101 - # 1 means the sensor is supported - supported = self.valueOf(sensors.PIDS) + # Find supported sensors - by getting PIDs from OBD (sensor zero) + # its a string of binary 01010101010101 + # 1 means the sensor is supported + supported = self.valueOf(sensors.PIDS) - # loop through PIDs binary - for i in range(len(supported)): - if supported[i] == "1": - sensor = sensors.by_PID[i] - sensor.supported = True - self.supportedSensors.append(sensor) + count = min(len(supported), len(sensors.by_PID)) + # loop through PIDs binary + for i in range(count): + if supported[i] == "1": + sensor = sensors.by_PID[i] + sensor.supported = True + self.supportedSensors.append(sensor) - def printSensors(self): - for sensor in self.sensors: - print str(sensor) - def hasSensor(sensor): - return sensor.supported + def printSensors(self): + for sensor in self.sensors: + print str(sensor) - def valueOf(sensor): - if self.hasSensor(sensor): - return self.port.get_sensor_value(sensor) - else: - return "Unsupported Sensor" + def hasSensor(sensor): + return sensor.supported + + def valueOf(sensor): + if self.hasSensor(sensor): + return self.port.get_sensor_value(sensor) + else: + return "Unsupported Sensor" if __name__ == "__main__": - o = OBD() - #o.connect() - time.sleep(3) - if not o.is_connected(): - print "Not connected" - else: - print "Connected to " + o.get_port_name() - #o.load_sensors() - o.printSensors() + o = OBD() + #o.connect() + time.sleep(3) + if not o.is_connected(): + print "Not connected" + else: + print "Connected to " + o.get_port_name() + #o.load_sensors() + o.printSensors() diff --git a/pyobd/obd_utils.py b/pyobd/obd_utils.py index b636e0da..4627b204 100644 --- a/pyobd/obd_utils.py +++ b/pyobd/obd_utils.py @@ -48,8 +48,7 @@ def scanSerial(): def hex_to_int(str): - i = eval("0x" + str, {}, {}) - return i + return int(str, 16) From f9c6ef2c867bfc172a1b738528616e87eda7dd2e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 20:04:37 -0400 Subject: [PATCH 025/569] changed import name --- README.md | 16 ++++++++-------- {pyobd => obd}/__init__.py | 0 {pyobd => obd}/obd.py | 0 {pyobd => obd}/obd_codes.py | 0 {pyobd => obd}/obd_port.py | 0 {pyobd => obd}/obd_sensors.py | 0 {pyobd => obd}/obd_utils.py | 0 7 files changed, 8 insertions(+), 8 deletions(-) rename {pyobd => obd}/__init__.py (100%) rename {pyobd => obd}/obd.py (100%) rename {pyobd => obd}/obd_codes.py (100%) rename {pyobd => obd}/obd_port.py (100%) rename {pyobd => obd}/obd_sensors.py (100%) rename {pyobd => obd}/obd_utils.py (100%) diff --git a/README.md b/README.md index b5035aae..9d965588 100644 --- a/README.md +++ b/README.md @@ -19,23 +19,23 @@ This library is forked from: After installing the library, simply import pyobd, and create a new OBD connection object: - import pyobd - connection = pyobd.OBD() # create connection object + import obd + connection = obd.OBD() # create connection object By default, pyOBD-IO will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. - import pyobd - connection = pyobd.OBD("/dev/ttyUSB0") # create connection with USB 0 + import obd + connection = obd.OBD("/dev/ttyUSB0") # create connection with USB 0 You can also use the scanSerial helper retrieve a list of connected ports - import pyobd + import obd - ports = pyobd.scanSerial() + ports = obd.scanSerial() print ports - connection = pyobd.OBD(ports[0]) # connect to the first port in the list + connection = obd.OBD(ports[0]) # connect to the first port in the list -Once a connection is made, pyOBD-IO will load a list of the available sensors in your car. A "Sensor" in pyOBD is an object containing its name, units, and retrival information. +Once a connection is made, pyOBD-IO will load a list of the available sensors in your car. A "Sensor" in pyOBD-IO is an object containing its name, units, and retrival functions. diff --git a/pyobd/__init__.py b/obd/__init__.py similarity index 100% rename from pyobd/__init__.py rename to obd/__init__.py diff --git a/pyobd/obd.py b/obd/obd.py similarity index 100% rename from pyobd/obd.py rename to obd/obd.py diff --git a/pyobd/obd_codes.py b/obd/obd_codes.py similarity index 100% rename from pyobd/obd_codes.py rename to obd/obd_codes.py diff --git a/pyobd/obd_port.py b/obd/obd_port.py similarity index 100% rename from pyobd/obd_port.py rename to obd/obd_port.py diff --git a/pyobd/obd_sensors.py b/obd/obd_sensors.py similarity index 100% rename from pyobd/obd_sensors.py rename to obd/obd_sensors.py diff --git a/pyobd/obd_utils.py b/obd/obd_utils.py similarity index 100% rename from pyobd/obd_utils.py rename to obd/obd_utils.py From bb5e752fb4f925b25b3a9fe4ce2f00c88180a60e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 20:28:04 -0400 Subject: [PATCH 026/569] fixed problem with PID sensors being unsupported at start --- obd/obd.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/obd/obd.py b/obd/obd.py index ee5831b7..af7ae3e3 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -14,7 +14,7 @@ class OBD(): def __init__(self, portstr=None): self.port = None - self.sensors = [] + self.supportedSensors = [] # initialize by connecting and loading sensors if self.connect(portstr): @@ -51,7 +51,7 @@ def get_port_name(self): def load_sensors(self): """ queries for available sensors, and compiles lists of indices and sensor objects """ - self.sensors = [] + self.supportedSensors = [] # Find supported sensors - by getting PIDs from OBD (sensor zero) # its a string of binary 01010101010101 @@ -69,17 +69,14 @@ def load_sensors(self): def printSensors(self): - for sensor in self.sensors: + for sensor in self.supportedSensors: print str(sensor) - def hasSensor(sensor): + def hasSensor(self, sensor): return sensor.supported - def valueOf(sensor): - if self.hasSensor(sensor): - return self.port.get_sensor_value(sensor) - else: - return "Unsupported Sensor" + def valueOf(self, sensor): + return self.port.get_sensor_value(sensor) From 2330b721bf7c484fdb77b8f997c71ce58bd8eff8 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 21:05:45 -0400 Subject: [PATCH 027/569] added sensor list and examples to readme --- README.md | 58 ++++++++++++++++++++++++++++++++++++++++------ obd/obd_sensors.py | 4 ++-- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9d965588..0d83ea60 100644 --- a/README.md +++ b/README.md @@ -17,27 +17,71 @@ This library is forked from: ### Usage -After installing the library, simply import pyobd, and create a new OBD connection object: +After installing the library, simply import pyobd, and create a new OBD connection object. By default, pyOBD-IO will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the scanSerial helper retrieve a list of connected ports import obd + connection = obd.OBD() # create connection object -By default, pyOBD-IO will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. + # OR - import obd connection = obd.OBD("/dev/ttyUSB0") # create connection with USB 0 -You can also use the scanSerial helper retrieve a list of connected ports - - import obd + # OR ports = obd.scanSerial() print ports connection = obd.OBD(ports[0]) # connect to the first port in the list -Once a connection is made, pyOBD-IO will load a list of the available sensors in your car. A "Sensor" in pyOBD-IO is an object containing its name, units, and retrival functions. +Once a connection is made, pyOBD-IO will load a list of the available sensors in your car. A "Sensor" in pyOBD-IO is an object containing its name, units, and retrieval functions. To get the value of a sensor, call the valueOf() function with a sensor object as an argument. + + import obd + + connection = obd.OBD() # create connection + + for sensor in connection.supportedSensors: + print str(sensor) # prints the sensor name + print connection.valueOf(sensor) + + +Sensors can also be explicitly targetted for values. The hasSensor() function will determine whether or not your car has the requested sensor. + + import obd + + connection = obd.OBD() # create connection object + if connection.hasSensor(obd.sensors.RPM): + print connection.valueOf(obd.sensors.RPM) + + +Here is a list of currently supported sensors with pyOBD-IO: + ++ Supported PIDs ++ S-S DTC Cleared ++ Calculated Engine Load ++ Engine Coolant Temperature ++ Short Term Fuel Trim - Bank 1 ++ Long Term Fuel Trim - Bank 1 ++ Short Term Fuel Trim - Bank 2 ++ Long Term Fuel Trim - Bank 2 ++ Fuel Pressure ++ Intake Manifold Pressure ++ Engine RPM ++ Vehicle Speed ++ Timing Advance ++ Intake Air Temp ++ Air Flow Rate (MAF) ++ Throttle Position ++ O2: Bank 1 - Sensor 1 ++ O2: Bank 1 - Sensor 2 ++ O2: Bank 1 - Sensor 3 ++ O2: Bank 1 - Sensor 4 ++ O2: Bank 2 - Sensor 1 ++ O2: Bank 2 - Sensor 2 ++ O2: Bank 2 - Sensor 3 ++ O2: Bank 2 - Sensor 4 ++ Engine Run Time Enjoy and drive safe! diff --git a/obd/obd_sensors.py b/obd/obd_sensors.py index e7632ca4..79812edb 100644 --- a/obd/obd_sensors.py +++ b/obd/obd_sensors.py @@ -182,8 +182,8 @@ class sensors(): Sensor("OBD_STANDARDS" , "OBD Standards Compliance" , "011C" , cpass , "" ), Sensor("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "011D" , cpass , "" ), Sensor("AUX_INPUT_STATUS" , "Auxiliary input status" , "011E" , cpass , "" ), - Sensor("RUN_TIME" , "Engine Start MIN" , "011F" , sec_to_min , "min" ), - #Sensor("RUN_TIME_MIL" , "Engine Run MIL" , "014D" , sec_to_min , "min" ), + Sensor("RUN_TIME" , "Engine Run Time" , "011F" , sec_to_min , "min" ), + #Sensor("RUN_TIME_MIL" , "Engine Run Time MIL" , "014D" , sec_to_min , "min" ), ] # assemble the dict, to access sensors by name From 3eb485cd00a283115e889a012e1b720363ba78eb Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 15 Oct 2014 21:09:51 -0400 Subject: [PATCH 028/569] readme comments --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0d83ea60..9c15484a 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ After installing the library, simply import pyobd, and create a new OBD connecti # OR - ports = obd.scanSerial() + ports = obd.scanSerial() # return list of valid USB or RF ports print ports connection = obd.OBD(ports[0]) # connect to the first port in the list @@ -38,26 +38,26 @@ Once a connection is made, pyOBD-IO will load a list of the available sensors in import obd - connection = obd.OBD() # create connection + connection = obd.OBD() for sensor in connection.supportedSensors: - print str(sensor) # prints the sensor name - print connection.valueOf(sensor) + print str(sensor) # prints the sensor name + print connection.valueOf(sensor) # gets and prints the sensor's value + print sensor.unit # prints the sensors units Sensors can also be explicitly targetted for values. The hasSensor() function will determine whether or not your car has the requested sensor. import obd - connection = obd.OBD() # create connection object + connection = obd.OBD() - if connection.hasSensor(obd.sensors.RPM): - print connection.valueOf(obd.sensors.RPM) + if connection.hasSensor(obd.sensors.RPM): # check for existance of sensor + print connection.valueOf(obd.sensors.RPM) # get value of sensor -Here is a list of currently supported sensors with pyOBD-IO: +Here are the currently supported sensors with pyOBD-IO: -+ Supported PIDs + S-S DTC Cleared + Calculated Engine Load + Engine Coolant Temperature From 36ffe64ae457de64be30565e4e7cc5ffd5e6ba4a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 16 Oct 2014 16:38:52 -0400 Subject: [PATCH 029/569] started refactoring sensors --- obd/obd_port.py | 39 ++---- obd/obd_sensors.py | 296 +++++++++++++++++++++++++-------------------- 2 files changed, 173 insertions(+), 162 deletions(-) diff --git a/obd/obd_port.py b/obd/obd_port.py index b9b6a411..05b71110 100644 --- a/obd/obd_port.py +++ b/obd/obd_port.py @@ -130,7 +130,7 @@ def close(self): self.ELMver = "Unknown" def send_command(self, cmd): - """Internal use only: not a public interface""" + if self.port: self.port.flushOutput() self.port.flushInput() @@ -140,7 +140,7 @@ def send_command(self, cmd): #print "Send command:" + cmd def interpret_result(self,code): - """Internal use only: not a public interface""" + # Code will be the string returned from the device. # It should look something like this: # '41 11 0 0\r\r' @@ -167,40 +167,23 @@ def interpret_result(self,code): return code def get_result(self): - """Internal use only: not a public interface""" - #time.sleep(0.01) - repeat_count = 0 + if self.port is not None: - buffer = "" + result = "" while 1: c = self.port.read(1) - if len(c) == 0: - if(repeat_count == 5): - break - print "Got nothing\n" - repeat_count = repeat_count + 1 - continue - - if c == '\r': + if not c or c == ">": + break + if c == "\x00": continue - - if c == ">": - break; - - if buffer != "" or c != ">": #if something is in buffer, add everything - buffer = buffer + c - - #print "Get result:" + buffer - if(buffer == ""): - return None - return buffer + result += c + return result else: - print "No port!" - return None + return "NORESPONSE" # get sensor value from command def get_sensor_value(self, sensor): - """Internal use only: not a public interface""" + cmd = sensor.cmd self.send_command(cmd) data = self.get_result() diff --git a/obd/obd_sensors.py b/obd/obd_sensors.py index 79812edb..ad851930 100644 --- a/obd/obd_sensors.py +++ b/obd/obd_sensors.py @@ -27,174 +27,202 @@ def maf(code): - code = hex_to_int(code) - return code * 0.00132276 + code = hex_to_int(code) + return code * 0.00132276 def throttle_pos(code): - code = hex_to_int(code) - return code * 100.0 / 255.0 + code = hex_to_int(code) + return code * 100.0 / 255.0 def fuel_pressure(code): # in kPa code = hex_to_int(code) return (code * 3) / 0.14504 def intake_pressure(code): # in kPa - code = hex_to_int(code) - return code / 0.14504 + code = hex_to_int(code) + return code / 0.14504 def rpm(code): - code = hex_to_int(code) - return code / 4 + code = hex_to_int(code) + return code / 4 def speed(code): - code = hex_to_int(code) - return code / 1.609 + code = hex_to_int(code) + return code / 1.609 def percent_scale(code): - code = hex_to_int(code) - return code * 100.0 / 255.0 + code = hex_to_int(code) + return code * 100.0 / 255.0 def timing_advance(code): - code = hex_to_int(code) - return (code - 128) / 2.0 + code = hex_to_int(code) + return (code - 128) / 2.0 def sec_to_min(code): - code = hex_to_int(code) - return code / 60 + code = hex_to_int(code) + return code / 60 def temp(code): - code = hex_to_int(code) - c = code - 40 - return 32 + (9 * c / 5) + code = hex_to_int(code) + c = code - 40 + return 32 + (9 * c / 5) def cpass(code): - #fixme - return code + #fixme + return code def fuel_trim_percent(code): - code = hex_to_int(code) - #return (code - 128.0) * 100.0 / 128 - return (code - 128) * 100 / 128 + code = hex_to_int(code) + #return (code - 128.0) * 100.0 / 128 + return (code - 128) * 100 / 128 def dtc_decrypt(code): - #first byte is byte after PID and without spaces - num = hex_to_int(code[:2]) #A byte - res = [] - - if num & 0x80: # is mil light on - mil = 1 - else: - mil = 0 - - # bit 0-6 are the number of dtc's. - num = num & 0x7f - - res.append(num) - res.append(mil) - - numB = hex_to_int(code[2:4]) #B byte - - for i in range(0,3): - res.append(((numB>>i)&0x01)+((numB>>(3+i))&0x02)) - - numC = hex_to_int(code[4:6]) #C byte - numD = hex_to_int(code[6:8]) #D byte - - for i in range(0,7): - res.append(((numC>>i)&0x01)+(((numD>>i)&0x01)<<1)) - - res.append(((numD>>7)&0x01)) #EGR SystemC7 bit of different - - #return res - return "#" + #first byte is byte after PID and without spaces + num = hex_to_int(code[:2]) #A byte + res = [] + + if num & 0x80: # is mil light on + mil = 1 + else: + mil = 0 + + # bit 0-6 are the number of dtc's. + num = num & 0x7f + + res.append(num) + res.append(mil) + + numB = hex_to_int(code[2:4]) #B byte + + for i in range(0,3): + res.append(((numB>>i)&0x01)+((numB>>(3+i))&0x02)) + + numC = hex_to_int(code[4:6]) #C byte + numD = hex_to_int(code[6:8]) #D byte + + for i in range(0,7): + res.append(((numC>>i)&0x01)+(((numD>>i)&0x01)<<1)) + + res.append(((numD>>7)&0x01)) #EGR SystemC7 bit of different + + #return res + return "#" def hex_to_bitstring(str): - bitstring = "" - for i in str: - # silly type safety, we don't want to eval random stuff - if type(i) == type(''): - v = eval("0x%s" % i) - if v & 8 : - bitstring += '1' - else: - bitstring += '0' - if v & 4: - bitstring += '1' - else: - bitstring += '0' - if v & 2: - bitstring += '1' - else: - bitstring += '0' - if v & 1: - bitstring += '1' - else: - bitstring += '0' - return bitstring - - -class Sensor: - def __init__(self, shortname, name, command, valueFunction, unit): - self.shortname = shortname - self.name = name - self.cmd = command - self.value = valueFunction - self.unit = unit - self.supported = False - - def __str__(self): - return self.name + bitstring = "" + for i in str: + # silly type safety, we don't want to eval random stuff + if type(i) == type(''): + v = eval("0x%s" % i) + if v & 8 : + bitstring += '1' + else: + bitstring += '0' + if v & 4: + bitstring += '1' + else: + bitstring += '0' + if v & 2: + bitstring += '1' + else: + bitstring += '0' + if v & 1: + bitstring += '1' + else: + bitstring += '0' + return bitstring + + + + + +class Units(): + NONE = None + BITSTRING = "Bit String" + PERCENT = "%" + VOLTS = "V" + DEGREES = "°" + SEC = "Sec" + MIN = "Min" + PSI = "PSI" + KPA = "KPA" + KPH = "KPH" + MPH = "MPH" + F = "F°" + C = "C°" + + +class Value(): + def __init__(self, value, unit): + self.value = value + self.unit = unit + + def __str__(self): + return "%s %s" % (str(self.value), str(self.unit)) + + +class OBDCommand(): + def __init__(self, shortname, name, mode, pid, expectedBytes, convertFunc, unit): + self.shortname = shortname + self.name = name + self.mode = mode # hex mode + self.pid = pid # hex PID + self.expectedBytes = expectedBytes # int number of bytes expected in return + self.convert = converterFunc + + def compute(result): + if "NODATA" in result: + return None + else: + if len(result) != self.expectedBytes: + print "Receieved unexpected number of bytes, trying to parse anyways..." + + # return the value object + return Value(self.convert(result), self.unit) class sensors(): # sensors must be in order of PID # note, the SHORTNAME field will be used as the dict key for that sensor - by_PID = [ - Sensor("PIDS" , "Supported PIDs" , "0100" , hex_to_bitstring , "" ), - Sensor("DTC_STATUS" , "S-S DTC Cleared" , "0101" , dtc_decrypt , "" ), - Sensor("DTC_FF" , "DTC C-F-F" , "0102" , cpass , "" ), - Sensor("FUEL_STATUS" , "Fuel System Status" , "0103" , cpass , "" ), - Sensor("LOAD" , "Calculated Engine Load" , "01041", percent_scale , "%" ), - Sensor("COOLANT_TEMP" , "Engine Coolant Temperature" , "0105" , temp , "F" ), - Sensor("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "0106" , fuel_trim_percent, "%" ), - Sensor("Long_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "0107" , fuel_trim_percent, "%" ), - Sensor("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "0108" , fuel_trim_percent, "%" ), - Sensor("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "0109" , fuel_trim_percent, "%" ), - Sensor("FUEL_PRESSURE" , "Fuel Pressure" , "010A" , fuel_pressure , "psi" ), - Sensor("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "010B" , intake_pressure , "psi" ), - Sensor("RPM" , "Engine RPM" , "010C1", rpm , "" ), - Sensor("SPEED" , "Vehicle Speed" , "010D1", speed , "MPH" ), - Sensor("TIMING_ADVANCE" , "Timing Advance" , "010E" , timing_advance , "degrees"), - Sensor("INTAKE_TEMP" , "Intake Air Temp" , "010F" , temp , "F" ), - Sensor("MAF" , "Air Flow Rate (MAF)" , "0110" , maf , "lb/min" ), - Sensor("THROTTLE" , "Throttle Position" , "01111", throttle_pos , "%" ), - Sensor("AIR_STATUS" , "Secondary Air Status" , "0112" , cpass , "" ), - Sensor("O2_SENSORS" , "O2 Sensors Present" , "0113" , cpass , "" ), - Sensor("O2_B1_S1" , "O2: Bank 1 - Sensor 1" , "0114" , fuel_trim_percent, "%" ), - Sensor("O2_B1_S2" , "O2: Bank 1 - Sensor 2" , "0115" , fuel_trim_percent, "%" ), - Sensor("O2_B1_S3" , "O2: Bank 1 - Sensor 3" , "0116" , fuel_trim_percent, "%" ), - Sensor("O2_B1_S4" , "O2: Bank 1 - Sensor 4" , "0117" , fuel_trim_percent, "%" ), - Sensor("O2_B2_S1" , "O2: Bank 2 - Sensor 1" , "0118" , fuel_trim_percent, "%" ), - Sensor("O2_B2_S2" , "O2: Bank 2 - Sensor 2" , "0119" , fuel_trim_percent, "%" ), - Sensor("O2_B2_S3" , "O2: Bank 2 - Sensor 3" , "011A" , fuel_trim_percent, "%" ), - Sensor("O2_B2_S4" , "O2: Bank 2 - Sensor 4" , "011B" , fuel_trim_percent, "%" ), - Sensor("OBD_STANDARDS" , "OBD Standards Compliance" , "011C" , cpass , "" ), - Sensor("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "011D" , cpass , "" ), - Sensor("AUX_INPUT_STATUS" , "Auxiliary input status" , "011E" , cpass , "" ), - Sensor("RUN_TIME" , "Engine Run Time" , "011F" , sec_to_min , "min" ), - #Sensor("RUN_TIME_MIL" , "Engine Run Time MIL" , "014D" , sec_to_min , "min" ), - ] - -# assemble the dict, to access sensors by name + by_PID = [ + # shortname name mode PID bytes convertFunc unit + OBDCommand("PIDS" , "Supported PIDs" , "01", "00" , 4, hex_to_bitstring , None ), + OBDCommand("DTC_STATUS" , "S-S DTC Cleared" , "01", "01" , 4, dtc_decrypt , None ), + OBDCommand("DTC_FF" , "DTC C-F-F" , "01", "02" , 2, cpass , None ), + OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03" , 2, cpass , None ), + OBDCommand("LOAD" , "Calculated Engine Load" , "01", "041", 1, percent_scale , Units.PERCENT ), + OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05" , 1, temp , Units.F ), + OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06" , 1, fuel_trim_percent, Units.PERCENT ), + OBDCommand("Long_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07" , 1, fuel_trim_percent, Units.PERCENT ), + OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08" , 1, fuel_trim_percent, Units.PERCENT ), + OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09" , 1, fuel_trim_percent, Units.PERCENT ), + OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A" , 1, fuel_pressure , Units.PSI ), + OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "01", "0B" , 1, intake_pressure , Units.PSI ), + OBDCommand("RPM" , "Engine RPM" , "01", "0C1", 2, rpm , None ), + OBDCommand("SPEED" , "Vehicle Speed" , "01", "0D1", 1, speed , Units.MPH ), + OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E" , 1, timing_advance , Units.DEGREES ), + OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F" , 1, temp , Units.F ), + OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10" , 2, maf , "lb/min" ), + OBDCommand("THROTTLE" , "Throttle Position" , "01", "111", 1, throttle_pos , Units.PERCENT ), + OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12" , 1, cpass , None ), + OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13" , 1, cpass , None ), + OBDCommand("O2_B1_S1" , "O2: Bank 1 - Sensor 1" , "01", "14" , 2, fuel_trim_percent, Units.PERCENT ), + OBDCommand("O2_B1_S2" , "O2: Bank 1 - Sensor 2" , "01", "15" , 2, fuel_trim_percent, Units.PERCENT ), + OBDCommand("O2_B1_S3" , "O2: Bank 1 - Sensor 3" , "01", "16" , 2, fuel_trim_percent, Units.PERCENT ), + OBDCommand("O2_B1_S4" , "O2: Bank 1 - Sensor 4" , "01", "17" , 2, fuel_trim_percent, Units.PERCENT ), + OBDCommand("O2_B2_S1" , "O2: Bank 2 - Sensor 1" , "01", "18" , 2, fuel_trim_percent, Units.PERCENT ), + OBDCommand("O2_B2_S2" , "O2: Bank 2 - Sensor 2" , "01", "19" , 2, fuel_trim_percent, Units.PERCENT ), + OBDCommand("O2_B2_S3" , "O2: Bank 2 - Sensor 3" , "01", "1A" , 2, fuel_trim_percent, Units.PERCENT ), + OBDCommand("O2_B2_S4" , "O2: Bank 2 - Sensor 4" , "01", "1B" , 2, fuel_trim_percent, Units.PERCENT ), + OBDCommand("OBD_STANDARDS" , "OBD Standards Compliance" , "01", "1C" , 1, cpass , None ), + OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D" , 1, cpass , None ), + OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E" , 1, cpass , None ), + OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F" , 2, sec_to_min , Units.MIN ), + #Sensor("RUN_TIME_MIL" , "Engine Run Time MIL" , "014D" , sec_to_min , "min" ), + ] + +# allow commands to be accessed by name for sensor in sensors.by_PID: sensors.__dict__[sensor.shortname] = sensor -#___________________________________________________________ - -def test(): - for i in SENSORS: - print i.name, i.value("F") - -if __name__ == "__main__": - test() From d471a1c10a5e74068a553c0a1d49701fd61eb7a3 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 16 Oct 2014 19:07:50 -0400 Subject: [PATCH 030/569] moved sensor response parsers to different file --- obd/{obd_codes.py => codes.py} | 0 obd/obd_sensors.py | 228 --------------------------------- obd/{obd_port.py => port.py} | 0 obd/preproccess.py | 110 ++++++++++++++++ obd/sensors.py | 94 ++++++++++++++ obd/{obd_utils.py => utils.py} | 26 ++++ 6 files changed, 230 insertions(+), 228 deletions(-) rename obd/{obd_codes.py => codes.py} (100%) delete mode 100644 obd/obd_sensors.py rename obd/{obd_port.py => port.py} (100%) create mode 100644 obd/preproccess.py create mode 100644 obd/sensors.py rename obd/{obd_utils.py => utils.py} (77%) diff --git a/obd/obd_codes.py b/obd/codes.py similarity index 100% rename from obd/obd_codes.py rename to obd/codes.py diff --git a/obd/obd_sensors.py b/obd/obd_sensors.py deleted file mode 100644 index ad851930..00000000 --- a/obd/obd_sensors.py +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/env python -########################################################################### -# obd_sensors.py -# -# Copyright 2004 Donour Sizemore (donour@uchicago.edu) -# Copyright 2009 Secons Ltd. (www.obdtester.com) -# -# This file is part of pyOBD. -# -# pyOBD is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# pyOBD is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with pyOBD; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -########################################################################### - - -from obd_utils import hex_to_int - - -def maf(code): - code = hex_to_int(code) - return code * 0.00132276 - -def throttle_pos(code): - code = hex_to_int(code) - return code * 100.0 / 255.0 - -def fuel_pressure(code): # in kPa - code = hex_to_int(code) - return (code * 3) / 0.14504 - -def intake_pressure(code): # in kPa - code = hex_to_int(code) - return code / 0.14504 - -def rpm(code): - code = hex_to_int(code) - return code / 4 - -def speed(code): - code = hex_to_int(code) - return code / 1.609 - -def percent_scale(code): - code = hex_to_int(code) - return code * 100.0 / 255.0 - -def timing_advance(code): - code = hex_to_int(code) - return (code - 128) / 2.0 - -def sec_to_min(code): - code = hex_to_int(code) - return code / 60 - -def temp(code): - code = hex_to_int(code) - c = code - 40 - return 32 + (9 * c / 5) - -def cpass(code): - #fixme - return code - -def fuel_trim_percent(code): - code = hex_to_int(code) - #return (code - 128.0) * 100.0 / 128 - return (code - 128) * 100 / 128 - -def dtc_decrypt(code): - #first byte is byte after PID and without spaces - num = hex_to_int(code[:2]) #A byte - res = [] - - if num & 0x80: # is mil light on - mil = 1 - else: - mil = 0 - - # bit 0-6 are the number of dtc's. - num = num & 0x7f - - res.append(num) - res.append(mil) - - numB = hex_to_int(code[2:4]) #B byte - - for i in range(0,3): - res.append(((numB>>i)&0x01)+((numB>>(3+i))&0x02)) - - numC = hex_to_int(code[4:6]) #C byte - numD = hex_to_int(code[6:8]) #D byte - - for i in range(0,7): - res.append(((numC>>i)&0x01)+(((numD>>i)&0x01)<<1)) - - res.append(((numD>>7)&0x01)) #EGR SystemC7 bit of different - - #return res - return "#" - -def hex_to_bitstring(str): - bitstring = "" - for i in str: - # silly type safety, we don't want to eval random stuff - if type(i) == type(''): - v = eval("0x%s" % i) - if v & 8 : - bitstring += '1' - else: - bitstring += '0' - if v & 4: - bitstring += '1' - else: - bitstring += '0' - if v & 2: - bitstring += '1' - else: - bitstring += '0' - if v & 1: - bitstring += '1' - else: - bitstring += '0' - return bitstring - - - - - -class Units(): - NONE = None - BITSTRING = "Bit String" - PERCENT = "%" - VOLTS = "V" - DEGREES = "°" - SEC = "Sec" - MIN = "Min" - PSI = "PSI" - KPA = "KPA" - KPH = "KPH" - MPH = "MPH" - F = "F°" - C = "C°" - - -class Value(): - def __init__(self, value, unit): - self.value = value - self.unit = unit - - def __str__(self): - return "%s %s" % (str(self.value), str(self.unit)) - - -class OBDCommand(): - def __init__(self, shortname, name, mode, pid, expectedBytes, convertFunc, unit): - self.shortname = shortname - self.name = name - self.mode = mode # hex mode - self.pid = pid # hex PID - self.expectedBytes = expectedBytes # int number of bytes expected in return - self.convert = converterFunc - - def compute(result): - if "NODATA" in result: - return None - else: - if len(result) != self.expectedBytes: - print "Receieved unexpected number of bytes, trying to parse anyways..." - - # return the value object - return Value(self.convert(result), self.unit) - - -class sensors(): - - # sensors must be in order of PID - # note, the SHORTNAME field will be used as the dict key for that sensor - by_PID = [ - # shortname name mode PID bytes convertFunc unit - OBDCommand("PIDS" , "Supported PIDs" , "01", "00" , 4, hex_to_bitstring , None ), - OBDCommand("DTC_STATUS" , "S-S DTC Cleared" , "01", "01" , 4, dtc_decrypt , None ), - OBDCommand("DTC_FF" , "DTC C-F-F" , "01", "02" , 2, cpass , None ), - OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03" , 2, cpass , None ), - OBDCommand("LOAD" , "Calculated Engine Load" , "01", "041", 1, percent_scale , Units.PERCENT ), - OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05" , 1, temp , Units.F ), - OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06" , 1, fuel_trim_percent, Units.PERCENT ), - OBDCommand("Long_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07" , 1, fuel_trim_percent, Units.PERCENT ), - OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08" , 1, fuel_trim_percent, Units.PERCENT ), - OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09" , 1, fuel_trim_percent, Units.PERCENT ), - OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A" , 1, fuel_pressure , Units.PSI ), - OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "01", "0B" , 1, intake_pressure , Units.PSI ), - OBDCommand("RPM" , "Engine RPM" , "01", "0C1", 2, rpm , None ), - OBDCommand("SPEED" , "Vehicle Speed" , "01", "0D1", 1, speed , Units.MPH ), - OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E" , 1, timing_advance , Units.DEGREES ), - OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F" , 1, temp , Units.F ), - OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10" , 2, maf , "lb/min" ), - OBDCommand("THROTTLE" , "Throttle Position" , "01", "111", 1, throttle_pos , Units.PERCENT ), - OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12" , 1, cpass , None ), - OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13" , 1, cpass , None ), - OBDCommand("O2_B1_S1" , "O2: Bank 1 - Sensor 1" , "01", "14" , 2, fuel_trim_percent, Units.PERCENT ), - OBDCommand("O2_B1_S2" , "O2: Bank 1 - Sensor 2" , "01", "15" , 2, fuel_trim_percent, Units.PERCENT ), - OBDCommand("O2_B1_S3" , "O2: Bank 1 - Sensor 3" , "01", "16" , 2, fuel_trim_percent, Units.PERCENT ), - OBDCommand("O2_B1_S4" , "O2: Bank 1 - Sensor 4" , "01", "17" , 2, fuel_trim_percent, Units.PERCENT ), - OBDCommand("O2_B2_S1" , "O2: Bank 2 - Sensor 1" , "01", "18" , 2, fuel_trim_percent, Units.PERCENT ), - OBDCommand("O2_B2_S2" , "O2: Bank 2 - Sensor 2" , "01", "19" , 2, fuel_trim_percent, Units.PERCENT ), - OBDCommand("O2_B2_S3" , "O2: Bank 2 - Sensor 3" , "01", "1A" , 2, fuel_trim_percent, Units.PERCENT ), - OBDCommand("O2_B2_S4" , "O2: Bank 2 - Sensor 4" , "01", "1B" , 2, fuel_trim_percent, Units.PERCENT ), - OBDCommand("OBD_STANDARDS" , "OBD Standards Compliance" , "01", "1C" , 1, cpass , None ), - OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D" , 1, cpass , None ), - OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E" , 1, cpass , None ), - OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F" , 2, sec_to_min , Units.MIN ), - #Sensor("RUN_TIME_MIL" , "Engine Run Time MIL" , "014D" , sec_to_min , "min" ), - ] - -# allow commands to be accessed by name -for sensor in sensors.by_PID: - sensors.__dict__[sensor.shortname] = sensor - diff --git a/obd/obd_port.py b/obd/port.py similarity index 100% rename from obd/obd_port.py rename to obd/port.py diff --git a/obd/preproccess.py b/obd/preproccess.py new file mode 100644 index 00000000..5e80da91 --- /dev/null +++ b/obd/preproccess.py @@ -0,0 +1,110 @@ + + +from utils import Value, Units, hex_to_int + + +def maf(code): + code = hex_to_int(code) + return code * 0.00132276 + +def throttle_pos(code): + code = hex_to_int(code) + return code * 100.0 / 255.0 + +def fuel_pressure(code): # in kPa + code = hex_to_int(code) + return (code * 3) / 0.14504 + +def intake_pressure(code): # in kPa + code = hex_to_int(code) + return code / 0.14504 + +def rpm(code): + code = hex_to_int(code) + return code / 4 + +def speed(code): + code = hex_to_int(code) + return code / 1.609 + +def percent_scale(code): + code = hex_to_int(code) + return code * 100.0 / 255.0 + +def timing_advance(code): + code = hex_to_int(code) + return (code - 128) / 2.0 + +def sec_to_min(code): + code = hex_to_int(code) + return code / 60 + +def temp(code): + code = hex_to_int(code) + c = code - 40 + return 32 + (9 * c / 5) + +def cpass(code): + #fixme + return code + +def fuel_trim_percent(code): + code = hex_to_int(code) + #return (code - 128.0) * 100.0 / 128 + return (code - 128) * 100 / 128 + +def dtc_decrypt(code): + #first byte is byte after PID and without spaces + num = hex_to_int(code[:2]) #A byte + res = [] + + if num & 0x80: # is mil light on + mil = 1 + else: + mil = 0 + + # bit 0-6 are the number of dtc's. + num = num & 0x7f + + res.append(num) + res.append(mil) + + numB = hex_to_int(code[2:4]) #B byte + + for i in range(0,3): + res.append(((numB>>i)&0x01)+((numB>>(3+i))&0x02)) + + numC = hex_to_int(code[4:6]) #C byte + numD = hex_to_int(code[6:8]) #D byte + + for i in range(0,7): + res.append(((numC>>i)&0x01)+(((numD>>i)&0x01)<<1)) + + res.append(((numD>>7)&0x01)) #EGR SystemC7 bit of different + + #return res + return "#" + +def hex_to_bitstring(str): + bitstring = "" + for i in str: + # silly type safety, we don't want to eval random stuff + if type(i) == type(''): + v = eval("0x%s" % i) + if v & 8 : + bitstring += '1' + else: + bitstring += '0' + if v & 4: + bitstring += '1' + else: + bitstring += '0' + if v & 2: + bitstring += '1' + else: + bitstring += '0' + if v & 1: + bitstring += '1' + else: + bitstring += '0' + return bitstring \ No newline at end of file diff --git a/obd/sensors.py b/obd/sensors.py new file mode 100644 index 00000000..bd16e7bf --- /dev/null +++ b/obd/sensors.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +########################################################################### +# obd_sensors.py +# +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) +# Copyright 2009 Secons Ltd. (www.obdtester.com) +# +# This file is part of pyOBD. +# +# pyOBD is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# pyOBD is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with pyOBD; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +########################################################################### + + + +from preprocess import * + + + +class OBDCommand(): + def __init__(self, shortname, name, mode, pid, expectedBytes, preprocessor): + self.shortname = shortname + self.name = name + self.mode = mode # hex mode + self.pid = pid # hex PID + self.expectedBytes = expectedBytes # int number of bytes expected in return + self.preprocess = preprocessor + + def compute(result): + if "NODATA" in result: + return None + else: + if len(result) != self.expectedBytes: + print "Receieved unexpected number of bytes, trying to parse anyways..." + + # return the value object + return self.preprocess(result) + + +class sensors(): + + # sensors must be in order of PID + # note, the SHORTNAME field will be used as the dict key for that sensor + by_PID = [ + # shortname name mode PID bytes preprocessor + OBDCommand("PIDS" , "Supported PIDs" , "01", "00" , 4, hex_to_bitstring ), + OBDCommand("DTC_STATUS" , "S-S DTC Cleared" , "01", "01" , 4, dtc_decrypt ), + OBDCommand("DTC_FF" , "DTC C-F-F" , "01", "02" , 2, cpass ), + OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03" , 2, cpass ), + OBDCommand("LOAD" , "Calculated Engine Load" , "01", "041", 1, percent_scale ), + OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05" , 1, temp ), + OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06" , 1, fuel_trim_percent ), + OBDCommand("Long_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07" , 1, fuel_trim_percent ), + OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08" , 1, fuel_trim_percent ), + OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09" , 1, fuel_trim_percent ), + OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A" , 1, fuel_pressure ), + OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "01", "0B" , 1, intake_pressure ), + OBDCommand("RPM" , "Engine RPM" , "01", "0C1", 2, rpm ), + OBDCommand("SPEED" , "Vehicle Speed" , "01", "0D1", 1, speed ), + OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E" , 1, timing_advance ), + OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F" , 1, temp ), + OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10" , 2, maf ), + OBDCommand("THROTTLE" , "Throttle Position" , "01", "111", 1, throttle_pos ), + OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12" , 1, cpass ), + OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13" , 1, cpass ), + OBDCommand("O2_B1_S1" , "O2: Bank 1 - Sensor 1" , "01", "14" , 2, fuel_trim_percent ), + OBDCommand("O2_B1_S2" , "O2: Bank 1 - Sensor 2" , "01", "15" , 2, fuel_trim_percent ), + OBDCommand("O2_B1_S3" , "O2: Bank 1 - Sensor 3" , "01", "16" , 2, fuel_trim_percent ), + OBDCommand("O2_B1_S4" , "O2: Bank 1 - Sensor 4" , "01", "17" , 2, fuel_trim_percent ), + OBDCommand("O2_B2_S1" , "O2: Bank 2 - Sensor 1" , "01", "18" , 2, fuel_trim_percent ), + OBDCommand("O2_B2_S2" , "O2: Bank 2 - Sensor 2" , "01", "19" , 2, fuel_trim_percent ), + OBDCommand("O2_B2_S3" , "O2: Bank 2 - Sensor 3" , "01", "1A" , 2, fuel_trim_percent ), + OBDCommand("O2_B2_S4" , "O2: Bank 2 - Sensor 4" , "01", "1B" , 2, fuel_trim_percent ), + OBDCommand("OBD_STANDARDS" , "OBD Standards Compliance" , "01", "1C" , 1, cpass ), + OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D" , 1, cpass ), + OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E" , 1, cpass ), + OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F" , 2, sec_to_min ), + #Sensor("RUN_TIME_MIL" , "Engine Run Time MIL" , "014D" , sec_to_min , "min" ), + ] + +# allow commands to be accessed by name +for sensor in sensors.by_PID: + sensors.__dict__[sensor.shortname] = sensor diff --git a/obd/obd_utils.py b/obd/utils.py similarity index 77% rename from obd/obd_utils.py rename to obd/utils.py index 4627b204..f87bfa68 100644 --- a/obd/obd_utils.py +++ b/obd/utils.py @@ -2,6 +2,32 @@ import errno + +class Units: + NONE = None + BITSTRING = "Bit String" + PERCENT = "Percent" + VOLT = "Volt" + F = "F" + C = "C" + SEC = "Second" + MIN = "Minute" + KPA = "kPa" + PSI = "PSI" + KPH = "KPH" + MPH = "MPH" + DEGREES = "Degrees" + + +class Value(): + def __init__(self, value, unit): + self.value = value + self.unit = unit + + def __str__(self): + return "%s %s" % (str(self.value), str(self.unit)) + + def tryPort(portStr): """returns boolean for port availability""" try: From d48697cf059fab0bc846f8cdd5bb301b8bca4776 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 16 Oct 2014 23:48:37 -0400 Subject: [PATCH 031/569] still reworking the sensors/commands --- obd/codes.py | 30 ++++++------ obd/commands.py | 113 +++++++++++++++++++++++++++++++++++++++++++++ obd/decoders.py | 95 +++++++++++++++++++++++++++++++++++++ obd/preproccess.py | 110 ------------------------------------------- obd/sensors.py | 94 ------------------------------------- obd/utils.py | 29 ++++++------ 6 files changed, 238 insertions(+), 233 deletions(-) create mode 100644 obd/commands.py create mode 100644 obd/decoders.py delete mode 100644 obd/preproccess.py delete mode 100644 obd/sensors.py diff --git a/obd/codes.py b/obd/codes.py index 77557b00..2e79425c 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -2094,8 +2094,8 @@ pcode_classes = { "P00XX": "Fuel and Air Metering and Auxiliary Emission Controls", - "P01XX": "Fuel and Air Merering", - "P02XX": "Fuel and Air Merering", + "P01XX": "Fuel and Air Metering", + "P02XX": "Fuel and Air Metering", "P03XX": "Ignition System or Misfire", "P04XX": "Auxiliary Emission Controls", "P05XX": "Vehicle Speed, Idle Control, and Auxiliary Inputs", @@ -2118,19 +2118,19 @@ } ptest= [ - "DTCs:", - "MIL:", + "DTCs", + "MIL", #A - "Misfire:", - "Fuel system:", - "Components:", + "Misfire", + "Fuel system", + "Components", #B,D - "Catalyst:", - "Heated Catalyst:", - "Evaporative system:", - "Secondary Air System:", - "A/C Refrigerant:" , - "Oxygen Sensor:", - "Oxygen Sensor Heater:", - "EGR SystemC7:" , + "Catalyst", + "Heated Catalyst", + "Evaporative system", + "Secondary Air System", + "A/C Refrigerant" , + "Oxygen Sensor", + "Oxygen Sensor Heater", + "EGR SystemC7" , ] diff --git a/obd/commands.py b/obd/commands.py new file mode 100644 index 00000000..7742d594 --- /dev/null +++ b/obd/commands.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +########################################################################### +# obd_sensors.py +# +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) +# Copyright 2009 Secons Ltd. (www.obdtester.com) +# +# This file is part of pyOBD. +# +# pyOBD is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# pyOBD is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with pyOBD; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +########################################################################### + + + +from decoders import * + + + +class OBDCommand(): + def __init__(self, sensorname, desc, cmd, pid, returnBytes, decoder): + self.sensorname = sensorname + self.desc = desc + self.cmd = cmd + self.bytes = returnBytes # number of bytes expected in return + self.decode = decoder + + def compute(result): + if "NODATA" in result: + return "" + else: + if (self.bytes > 0) and (len(result) != self.bytes * 2): + print "Receieved unexpected number of bytes, trying to parse anyways..." + + # return the decoded value object + return self.decode(result) + + + + +# note, the SENSOR NAME field will be used as the dict key for that sensor +# if no sensor name is given, it will be treated as a special command +commands = [ + # sensor name description cmd bytes decoder + OBDCommand("PIDS" , "Supported PIDs" , "0100" , 4, noop ), + OBDCommand("STATUS" , "Status since DTCs cleared" , "0101" , 4, noop ), + OBDCommand("FREEZE_DTC" , "Freeze DTC" , "0102" , 2, noop ), + OBDCommand("FUEL_STATUS" , "Fuel System Status" , "0103" , 2, noop ), + OBDCommand("LOAD" , "Calculated Engine Load" , "0104" , 1, percent ), + OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "0105" , 1, temp ), + OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "0106" , 1, percent_centered ), + OBDCommand("Long_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "0107" , 1, percent_centered ), + OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "0108" , 1, percent_centered ), + OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "0109" , 1, percent_centered ), + OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "010A" , 1, fuel_pressure ), + OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "010B" , 1, intake_pressure ), + OBDCommand("RPM" , "Engine RPM" , "010C" , 2, rpm ), + OBDCommand("SPEED" , "Vehicle Speed" , "010D" , 1, speed ), + OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "010E" , 1, timing_advance ), + OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "010F" , 1, temp ), + OBDCommand("MAF" , "Air Flow Rate (MAF)" , "0110" , 2, maf ), + OBDCommand("THROTTLE" , "Throttle Position" , "0111" , 1, percent ), + OBDCommand("AIR_STATUS" , "Secondary Air Status" , "0112" , 1, noop ), + OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "0113" , 1, noop ), + OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1" , "0114" , 2, sensor_voltage ), + OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2" , "0115" , 2, sensor_voltage ), + OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3" , "0116" , 2, sensor_voltage ), + OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4" , "0117" , 2, sensor_voltage ), + OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1" , "0118" , 2, sensor_voltage ), + OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2" , "0119" , 2, sensor_voltage ), + OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3" , "011A" , 2, sensor_voltage ), + OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4" , "011B" , 2, sensor_voltage ), + OBDCommand("OBD_STANDARDS" , "OBD Standards Compliance" , "011C" , 1, noop ), + OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "011D" , 1, noop ), + OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "011E" , 1, noop ), + OBDCommand("RUN_TIME" , "Engine Run Time" , "011F" , 2, sec_to_min ), + #OBDCommand("RUN_TIME_MIL" , "Engine Run Time MIL" , "014D" , sec_to_min ), + + + # DTC handling + # sensor name description cmd bytes decoder + OBDCommand("GET_DTC" , "Get DTCs" , "03" , 0, noop ), + OBDCommand("CLEAR_DTC" , "Clear DTCs" , "04" , 0, noop ), + OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07" , 0, noop ), +] + + + +class sensors(): + pass + +class specials(): + pass + + +# allow sensor commands to be accessed by name +for command in commands: + # if the command has no decoder, it is considered a special + if command.decode == noop: + specials.__dict__[command.sensorname] = command + else: + sensors.__dict__[command.sensorname] = command diff --git a/obd/decoders.py b/obd/decoders.py new file mode 100644 index 00000000..79769f67 --- /dev/null +++ b/obd/decoders.py @@ -0,0 +1,95 @@ + + +from utils import Value, Unit + + +def unhex(_hex): + return int(_hex, 16) + +def unbin(_bin): + return int(_bin, 2) + +def bitstring(_hex): + return bin(unhex(_hex))[2:] + + + + +# functions accepting hex responses from the OBD connection, and computing/returning values with units + +def noop(_hex): + return Value(_hex, Unit.NONE) + +# 0 to 100 % +def percent(_hex): + v = unhex(_hex) + v = v * 100.0 / 255.0 + return Value(v, Unit.PERCENT) + +# -100 to 100 % +def percent_centered(_hex): + v = unhex(_hex) + v = (v - 128) * 100.0 / 128.0 + return Value(v, Unit.PERCENT) + +# -40 to 215 C +def temp(_hex): + v = unhex(_hex) + v = v - 40 + return Value(v, Unit.C) + +# 0 to 1.275 volts +def sensor_voltage(_hex): + v = unhex(_hex[0:2]) + return Value(v, Unit.VOLT) + +# 0 to 765 kPa +def fuel_pressure(_hex): + v = unhex(_hex) + v = v * 3 + return Value(v, Unit.KPA) + +# 0 to 255 kPa +def intake_pressure(_hex): + v = unhex(_hex) + return Value(v, Unit.KPA) + +# 0 to 16,383.75 RPM +def rpm(_hex): + v = unhex(_hex) + v = v / 4.0 + return Value(v, Unit.RPM) + +# 0 to 255 KPH +def speed(_hex): + v = unhex(_hex) + return Value(v, Unit.KPH) + +# -64 to 63.5 +def timing_advance(_hex): + v = unhex(_hex) + v = (v - 128) / 2.0 + return Value(v, Unit.DEGREES) + +# 0 to 655.35 grams/sec +def maf(_hex): + v = unhex(_hex) + v = v / 100.0 + return Value(v, Unit.GRAM_P_SEC) + + +# these functions draw data from the same PID +def mil(_hex): + v = bitstring(_hex) + return v[0] == '1' + +def dtc_count(_hex): + v = bitstring(_hex) + return unbin(v[1:8]) + + + + +def special_PID_01(_hex): + + diff --git a/obd/preproccess.py b/obd/preproccess.py deleted file mode 100644 index 5e80da91..00000000 --- a/obd/preproccess.py +++ /dev/null @@ -1,110 +0,0 @@ - - -from utils import Value, Units, hex_to_int - - -def maf(code): - code = hex_to_int(code) - return code * 0.00132276 - -def throttle_pos(code): - code = hex_to_int(code) - return code * 100.0 / 255.0 - -def fuel_pressure(code): # in kPa - code = hex_to_int(code) - return (code * 3) / 0.14504 - -def intake_pressure(code): # in kPa - code = hex_to_int(code) - return code / 0.14504 - -def rpm(code): - code = hex_to_int(code) - return code / 4 - -def speed(code): - code = hex_to_int(code) - return code / 1.609 - -def percent_scale(code): - code = hex_to_int(code) - return code * 100.0 / 255.0 - -def timing_advance(code): - code = hex_to_int(code) - return (code - 128) / 2.0 - -def sec_to_min(code): - code = hex_to_int(code) - return code / 60 - -def temp(code): - code = hex_to_int(code) - c = code - 40 - return 32 + (9 * c / 5) - -def cpass(code): - #fixme - return code - -def fuel_trim_percent(code): - code = hex_to_int(code) - #return (code - 128.0) * 100.0 / 128 - return (code - 128) * 100 / 128 - -def dtc_decrypt(code): - #first byte is byte after PID and without spaces - num = hex_to_int(code[:2]) #A byte - res = [] - - if num & 0x80: # is mil light on - mil = 1 - else: - mil = 0 - - # bit 0-6 are the number of dtc's. - num = num & 0x7f - - res.append(num) - res.append(mil) - - numB = hex_to_int(code[2:4]) #B byte - - for i in range(0,3): - res.append(((numB>>i)&0x01)+((numB>>(3+i))&0x02)) - - numC = hex_to_int(code[4:6]) #C byte - numD = hex_to_int(code[6:8]) #D byte - - for i in range(0,7): - res.append(((numC>>i)&0x01)+(((numD>>i)&0x01)<<1)) - - res.append(((numD>>7)&0x01)) #EGR SystemC7 bit of different - - #return res - return "#" - -def hex_to_bitstring(str): - bitstring = "" - for i in str: - # silly type safety, we don't want to eval random stuff - if type(i) == type(''): - v = eval("0x%s" % i) - if v & 8 : - bitstring += '1' - else: - bitstring += '0' - if v & 4: - bitstring += '1' - else: - bitstring += '0' - if v & 2: - bitstring += '1' - else: - bitstring += '0' - if v & 1: - bitstring += '1' - else: - bitstring += '0' - return bitstring \ No newline at end of file diff --git a/obd/sensors.py b/obd/sensors.py deleted file mode 100644 index bd16e7bf..00000000 --- a/obd/sensors.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python -########################################################################### -# obd_sensors.py -# -# Copyright 2004 Donour Sizemore (donour@uchicago.edu) -# Copyright 2009 Secons Ltd. (www.obdtester.com) -# -# This file is part of pyOBD. -# -# pyOBD is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# pyOBD is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with pyOBD; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -########################################################################### - - - -from preprocess import * - - - -class OBDCommand(): - def __init__(self, shortname, name, mode, pid, expectedBytes, preprocessor): - self.shortname = shortname - self.name = name - self.mode = mode # hex mode - self.pid = pid # hex PID - self.expectedBytes = expectedBytes # int number of bytes expected in return - self.preprocess = preprocessor - - def compute(result): - if "NODATA" in result: - return None - else: - if len(result) != self.expectedBytes: - print "Receieved unexpected number of bytes, trying to parse anyways..." - - # return the value object - return self.preprocess(result) - - -class sensors(): - - # sensors must be in order of PID - # note, the SHORTNAME field will be used as the dict key for that sensor - by_PID = [ - # shortname name mode PID bytes preprocessor - OBDCommand("PIDS" , "Supported PIDs" , "01", "00" , 4, hex_to_bitstring ), - OBDCommand("DTC_STATUS" , "S-S DTC Cleared" , "01", "01" , 4, dtc_decrypt ), - OBDCommand("DTC_FF" , "DTC C-F-F" , "01", "02" , 2, cpass ), - OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03" , 2, cpass ), - OBDCommand("LOAD" , "Calculated Engine Load" , "01", "041", 1, percent_scale ), - OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05" , 1, temp ), - OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06" , 1, fuel_trim_percent ), - OBDCommand("Long_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07" , 1, fuel_trim_percent ), - OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08" , 1, fuel_trim_percent ), - OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09" , 1, fuel_trim_percent ), - OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A" , 1, fuel_pressure ), - OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "01", "0B" , 1, intake_pressure ), - OBDCommand("RPM" , "Engine RPM" , "01", "0C1", 2, rpm ), - OBDCommand("SPEED" , "Vehicle Speed" , "01", "0D1", 1, speed ), - OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E" , 1, timing_advance ), - OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F" , 1, temp ), - OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10" , 2, maf ), - OBDCommand("THROTTLE" , "Throttle Position" , "01", "111", 1, throttle_pos ), - OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12" , 1, cpass ), - OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13" , 1, cpass ), - OBDCommand("O2_B1_S1" , "O2: Bank 1 - Sensor 1" , "01", "14" , 2, fuel_trim_percent ), - OBDCommand("O2_B1_S2" , "O2: Bank 1 - Sensor 2" , "01", "15" , 2, fuel_trim_percent ), - OBDCommand("O2_B1_S3" , "O2: Bank 1 - Sensor 3" , "01", "16" , 2, fuel_trim_percent ), - OBDCommand("O2_B1_S4" , "O2: Bank 1 - Sensor 4" , "01", "17" , 2, fuel_trim_percent ), - OBDCommand("O2_B2_S1" , "O2: Bank 2 - Sensor 1" , "01", "18" , 2, fuel_trim_percent ), - OBDCommand("O2_B2_S2" , "O2: Bank 2 - Sensor 2" , "01", "19" , 2, fuel_trim_percent ), - OBDCommand("O2_B2_S3" , "O2: Bank 2 - Sensor 3" , "01", "1A" , 2, fuel_trim_percent ), - OBDCommand("O2_B2_S4" , "O2: Bank 2 - Sensor 4" , "01", "1B" , 2, fuel_trim_percent ), - OBDCommand("OBD_STANDARDS" , "OBD Standards Compliance" , "01", "1C" , 1, cpass ), - OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D" , 1, cpass ), - OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E" , 1, cpass ), - OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F" , 2, sec_to_min ), - #Sensor("RUN_TIME_MIL" , "Engine Run Time MIL" , "014D" , sec_to_min , "min" ), - ] - -# allow commands to be accessed by name -for sensor in sensors.by_PID: - sensors.__dict__[sensor.shortname] = sensor diff --git a/obd/utils.py b/obd/utils.py index f87bfa68..a30931ae 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -3,20 +3,21 @@ -class Units: - NONE = None - BITSTRING = "Bit String" - PERCENT = "Percent" - VOLT = "Volt" - F = "F" - C = "C" - SEC = "Second" - MIN = "Minute" - KPA = "kPa" - PSI = "PSI" - KPH = "KPH" - MPH = "MPH" - DEGREES = "Degrees" +class Unit: + NONE = None + BITSTRING = "Bit String" + PERCENT = "Percent" + VOLT = "Volt" + F = "F" + C = "C" + SEC = "Second" + MIN = "Minute" + KPA = "kPa" + PSI = "PSI" + KPH = "KPH" + MPH = "MPH" + DEGREES = "Degrees" + GRAM_P_SEC = "Grams per Second" class Value(): From cb5ccc10120af7e34cce5fa4d51d4e814b9de36b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 17 Oct 2014 14:39:40 -0400 Subject: [PATCH 032/569] organized sensors by mode --- obd/__init__.py | 4 +- obd/commands.py | 148 +++++++++++++++++++++++++++++++----------------- obd/obd.py | 32 +++++------ 3 files changed, 115 insertions(+), 69 deletions(-) diff --git a/obd/__init__.py b/obd/__init__.py index 541524d9..055542ab 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -1,4 +1,4 @@ from obd import OBD -from obd_sensors import sensors -from obd_utils import scanSerial +from commands import sensors, specials, commands +from utils import scanSerial diff --git a/obd/commands.py b/obd/commands.py index 7742d594..c021a77d 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -29,13 +29,25 @@ class OBDCommand(): - def __init__(self, sensorname, desc, cmd, pid, returnBytes, decoder): + def __init__(self, sensorname, desc, mode, pid, pid, returnBytes, decoder): self.sensorname = sensorname self.desc = desc - self.cmd = cmd + self.mode = mode + self.pid = pid self.bytes = returnBytes # number of bytes expected in return self.decode = decoder + def clone(self): + return OBDCommand(self.sensorname, + self.desc, + self.mode, + self.pid, + self.bytes, + self.decode) + + def getCommand(self): + return self.mode + self.pid + def compute(result): if "NODATA" in result: return "" @@ -50,53 +62,86 @@ def compute(result): # note, the SENSOR NAME field will be used as the dict key for that sensor -# if no sensor name is given, it will be treated as a special command -commands = [ - # sensor name description cmd bytes decoder - OBDCommand("PIDS" , "Supported PIDs" , "0100" , 4, noop ), - OBDCommand("STATUS" , "Status since DTCs cleared" , "0101" , 4, noop ), - OBDCommand("FREEZE_DTC" , "Freeze DTC" , "0102" , 2, noop ), - OBDCommand("FUEL_STATUS" , "Fuel System Status" , "0103" , 2, noop ), - OBDCommand("LOAD" , "Calculated Engine Load" , "0104" , 1, percent ), - OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "0105" , 1, temp ), - OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "0106" , 1, percent_centered ), - OBDCommand("Long_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "0107" , 1, percent_centered ), - OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "0108" , 1, percent_centered ), - OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "0109" , 1, percent_centered ), - OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "010A" , 1, fuel_pressure ), - OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "010B" , 1, intake_pressure ), - OBDCommand("RPM" , "Engine RPM" , "010C" , 2, rpm ), - OBDCommand("SPEED" , "Vehicle Speed" , "010D" , 1, speed ), - OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "010E" , 1, timing_advance ), - OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "010F" , 1, temp ), - OBDCommand("MAF" , "Air Flow Rate (MAF)" , "0110" , 2, maf ), - OBDCommand("THROTTLE" , "Throttle Position" , "0111" , 1, percent ), - OBDCommand("AIR_STATUS" , "Secondary Air Status" , "0112" , 1, noop ), - OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "0113" , 1, noop ), - OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1" , "0114" , 2, sensor_voltage ), - OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2" , "0115" , 2, sensor_voltage ), - OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3" , "0116" , 2, sensor_voltage ), - OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4" , "0117" , 2, sensor_voltage ), - OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1" , "0118" , 2, sensor_voltage ), - OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2" , "0119" , 2, sensor_voltage ), - OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3" , "011A" , 2, sensor_voltage ), - OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4" , "011B" , 2, sensor_voltage ), - OBDCommand("OBD_STANDARDS" , "OBD Standards Compliance" , "011C" , 1, noop ), - OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "011D" , 1, noop ), - OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "011E" , 1, noop ), - OBDCommand("RUN_TIME" , "Engine Run Time" , "011F" , 2, sec_to_min ), - #OBDCommand("RUN_TIME_MIL" , "Engine Run Time MIL" , "014D" , sec_to_min ), - - - # DTC handling - # sensor name description cmd bytes decoder - OBDCommand("GET_DTC" , "Get DTCs" , "03" , 0, noop ), - OBDCommand("CLEAR_DTC" , "Clear DTCs" , "04" , 0, noop ), - OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07" , 0, noop ), + +__mode1__ = [ + # mode 1 + # sensor name description mode cmd bytes decoder + OBDCommand("PIDS" , "Supported PIDs" , "01", "00" , 4, noop ), + OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01" , 4, noop ), + OBDCommand("FREEZE_DTC" , "Freeze DTC" , "01", "02" , 2, noop ), + OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03" , 2, noop ), + OBDCommand("LOAD" , "Calculated Engine Load" , "01", "04" , 1, percent ), + OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05" , 1, temp ), + OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06" , 1, percent_centered ), + OBDCommand("Long_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07" , 1, percent_centered ), + OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08" , 1, percent_centered ), + OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09" , 1, percent_centered ), + OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A" , 1, fuel_pressure ), + OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "01", "0B" , 1, intake_pressure ), + OBDCommand("RPM" , "Engine RPM" , "01", "0C" , 2, rpm ), + OBDCommand("SPEED" , "Vehicle Speed" , "01", "0D" , 1, speed ), + OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E" , 1, timing_advance ), + OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F" , 1, temp ), + OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10" , 2, maf ), + OBDCommand("THROTTLE" , "Throttle Position" , "01", "11" , 1, percent ), + OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12" , 1, noop ), + OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13" , 1, noop ), + OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1" , "01", "14" , 2, sensor_voltage ), + OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2" , "01", "15" , 2, sensor_voltage ), + OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3" , "01", "16" , 2, sensor_voltage ), + OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4" , "01", "17" , 2, sensor_voltage ), + OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1" , "01", "18" , 2, sensor_voltage ), + OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2" , "01", "19" , 2, sensor_voltage ), + OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3" , "01", "1A" , 2, sensor_voltage ), + OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4" , "01", "1B" , 2, sensor_voltage ), + OBDCommand("OBD_STANDARDS" , "OBD Standards Compliance" , "01", "1C" , 1, noop ), + OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D" , 1, noop ), + OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E" , 1, noop ), + OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F" , 2, sec_to_min ), + #OBDCommand("RUN_TIME_MIL" , "Engine Run Time MIL" , "01", "4D" , sec_to_min ), +] + +# mode 2 is the same as mode 1, but returns values from when the DTC occured +__mode2__ = [] +for c in __mode1__: + c = c.clone() + c.mode = "02" + c.name = "DTC_" + c.name + c.desc = "DTC " + c.desc + __mode2__.append(c) + + +__mode3__ = [ + # sensor name description mode cmd bytes decoder + OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 0, noop ), +] + +__mode4__ = [ + # sensor name description mode cmd bytes decoder + OBDCommand("CLEAR_DTC" , "Clear DTCs" , "04", "" , 0, noop ), +] + +__mode7__ [ + # sensor name description mode cmd bytes decoder + OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", "" , 0, noop ), ] + +# assemble the commands by mode +commands = [ + [], + __mode1__, + __mode2__, + __mode3__, + __mode4__, + [], + [], + __mode7__ +] + + class sensors(): pass @@ -105,9 +150,10 @@ class specials(): # allow sensor commands to be accessed by name -for command in commands: - # if the command has no decoder, it is considered a special - if command.decode == noop: - specials.__dict__[command.sensorname] = command - else: - sensors.__dict__[command.sensorname] = command +for m in commands: + for c in m: + # if the command has no decoder, it is considered a special + if c.decode == noop: + specials.__dict__[c.sensorname] = c + else: + sensors.__dict__[c.sensorname] = c diff --git a/obd/obd.py b/obd/obd.py index af7ae3e3..44ffc983 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -2,9 +2,8 @@ import time -from obd_port import State -from obd_port import OBDPort -from obd_sensors import sensors +from port import OBDPort, State +from commands import sensors, specials, commands from obd_utils import scanSerial @@ -14,7 +13,7 @@ class OBD(): def __init__(self, portstr=None): self.port = None - self.supportedSensors = [] + self.supportedCommands = [] # initialize by connecting and loading sensors if self.connect(portstr): @@ -51,35 +50,36 @@ def get_port_name(self): def load_sensors(self): """ queries for available sensors, and compiles lists of indices and sensor objects """ - self.supportedSensors = [] + self.supportedCommands = [] # Find supported sensors - by getting PIDs from OBD (sensor zero) # its a string of binary 01010101010101 # 1 means the sensor is supported - supported = self.valueOf(sensors.PIDS) + supported = self.sendCommand(commands[1][0]) # mode 01, command 00 - count = min(len(supported), len(sensors.by_PID)) + count = min(len(supported), len(commands[1])) # loop through PIDs binary for i in range(count): if supported[i] == "1": - sensor = sensors.by_PID[i] - sensor.supported = True - self.supportedSensors.append(sensor) + c = commands[1][i] + c.supported = True + self.supportedCommands.append(c) - def printSensors(self): - for sensor in self.supportedSensors: - print str(sensor) + def printCommands(self): + for c in self.supportedCommands: + print str(c) - def hasSensor(self, sensor): - return sensor.supported + def hasCommand(self, c): + return c.supported - def valueOf(self, sensor): + def sendCommand(self, command): return self.port.get_sensor_value(sensor) + if __name__ == "__main__": o = OBD() From fac9ee3e953bd79c8092dcfe63ee78a13b109554 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 17 Oct 2014 15:37:03 -0400 Subject: [PATCH 033/569] wrote more consise DTC decoders --- obd/codes.py | 26 +------------------------- obd/commands.py | 2 +- obd/decoders.py | 47 +++++++++++++++++++++++++++++------------------ obd/utils.py | 47 +++++++++++------------------------------------ 4 files changed, 42 insertions(+), 80 deletions(-) diff --git a/obd/codes.py b/obd/codes.py index 2e79425c..afdb89db 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -22,7 +22,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ########################################################################### -pcodes = { +DTC_CODES = { "P0001": "Fuel Volume Regulator Control Circuit/Open", "P0002": "Fuel Volume Regulator Control Circuit Range/Performance", "P0003": "Fuel Volume Regulator Control Circuit Low", @@ -2092,30 +2092,6 @@ } -pcode_classes = { - "P00XX": "Fuel and Air Metering and Auxiliary Emission Controls", - "P01XX": "Fuel and Air Metering", - "P02XX": "Fuel and Air Metering", - "P03XX": "Ignition System or Misfire", - "P04XX": "Auxiliary Emission Controls", - "P05XX": "Vehicle Speed, Idle Control, and Auxiliary Inputs", - "P06XX": "Computer and Auxiliary Outputs", - "P07XX": "Transmission", - "P08XX": "Transmission", - "P09XX": "Transmission", - "P0AXX": "Hybrid Propulsion", - "P10XX": "Manufacturer Controlled Fuel and Air Metering and Auxiliary Emission Controls", - "P11XX": "Manufacturer Controlled Fuel and Air Merering", - "P12XX": "Fuel and Air Merering", - "P13XX": "Ignition System or Misfire", - "P14XX": "Auxiliary Emission Controls", - "P15XX": "Vehicle Speed, Idle Control, and Auxiliary Inputs", - "P16XX": "Computer and Auxiliary Outputs", - "P17XX": "Transmission", - "P18XX": "Transmission", - "P19XX": "Transmission", - - } ptest= [ "DTCs", diff --git a/obd/commands.py b/obd/commands.py index c021a77d..4b95d01e 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -97,7 +97,7 @@ def compute(result): OBDCommand("OBD_STANDARDS" , "OBD Standards Compliance" , "01", "1C" , 1, noop ), OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D" , 1, noop ), OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E" , 1, noop ), - OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F" , 2, sec_to_min ), + OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F" , 2, seconds ), #OBDCommand("RUN_TIME_MIL" , "Engine Run Time MIL" , "01", "4D" , sec_to_min ), ] diff --git a/obd/decoders.py b/obd/decoders.py index 79769f67..6edea7cd 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -1,17 +1,6 @@ -from utils import Value, Unit - - -def unhex(_hex): - return int(_hex, 16) - -def unbin(_bin): - return int(_bin, 2) - -def bitstring(_hex): - return bin(unhex(_hex))[2:] - +from utils import Value, Unit, unhex, unbin, bitstring @@ -77,6 +66,11 @@ def maf(_hex): v = v / 100.0 return Value(v, Unit.GRAM_P_SEC) +# 0 to 655.35 seconds +def seconds(_hex): + v = unhex(_hex) + return Value(v, Unit.SECONDS) + # these functions draw data from the same PID def mil(_hex): @@ -87,9 +81,26 @@ def dtc_count(_hex): v = bitstring(_hex) return unbin(v[1:8]) - - - -def special_PID_01(_hex): - - +# converts 2 bytes of hex into a DTC code +def dtc(_hex): + dtc = "" + bits = bitstring(_hex[0]) + + dtc += ['P', 'C', 'B', 'U'][unbin(bits[0:2]))] + dtc += str(unbin(bits[2:4])) + dtc += _hex[1:4] + return dtc + +# converts a frame of 2-byte DTCs into a list of DTCs +def dtc_frame(_hex): + code_length = 4 # number of hex chars consumed by one code + size = len(_hex / 4) # number of codes defined in THIS FRAME (not total) + codes = [] + for n in range(size): + + start = code_length * n + end = start + code_length + + codes.append(dtc(_hex[start:end])) + + return codes diff --git a/obd/utils.py b/obd/utils.py index a30931ae..cf2edf58 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -6,6 +6,7 @@ class Unit: NONE = None BITSTRING = "Bit String" + DTC = "Diagnostic Trouble Code" PERCENT = "Percent" VOLT = "Volt" F = "F" @@ -29,6 +30,16 @@ def __str__(self): return "%s %s" % (str(self.value), str(self.unit)) +def unhex(_hex): + return int(_hex, 16) + +def unbin(_bin): + return int(_bin, 2) + +def bitstring(_hex): + return bin(unhex(_hex))[2:] + + def tryPort(portStr): """returns boolean for port availability""" try: @@ -71,39 +82,3 @@ def scanSerial(): ''' return available - - - -def hex_to_int(str): - return int(str, 16) - - - -def decrypt_dtc_code(code): - """Returns the 5-digit DTC code from hex encoding""" - dtc = [] - current = code - for i in range(0,3): - if len(current)<4: - raise "Tried to decode bad DTC: %s" % code - - tc = hex_to_int(current[0]) #typecode - tc = tc >> 2 - if tc == 0: - type = "P" - elif tc == 1: - type = "C" - elif tc == 2: - type = "B" - elif tc == 3: - type = "U" - else: - raise tc - - dig1 = str(hex_to_int(current[0]) & 3) - dig2 = str(hex_to_int(current[1])) - dig3 = str(hex_to_int(current[2])) - dig4 = str(hex_to_int(current[3])) - dtc.append(type+dig1+dig2+dig3+dig4) - current = current[4:] - return dtc From 74a7ccbf865cb8bab6d50a9c449618bf7cd43ded Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 17 Oct 2014 22:37:36 -0400 Subject: [PATCH 034/569] finished status decoding --- obd/codes.py | 720 +++++++++++++++++++++++++++--------------------- obd/commands.py | 4 +- obd/decoders.py | 80 +++++- obd/utils.py | 18 +- 4 files changed, 497 insertions(+), 325 deletions(-) diff --git a/obd/codes.py b/obd/codes.py index afdb89db..c2a44905 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -22,7 +22,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ########################################################################### -DTC_CODES = { +DTC = { "P0001": "Fuel Volume Regulator Control Circuit/Open", "P0002": "Fuel Volume Regulator Control Circuit Range/Performance", "P0003": "Fuel Volume Regulator Control Circuit Low", @@ -1790,323 +1790,411 @@ "P3495": "Cylinder 12 Exhaust Valve Control Circuit Low", "P3496": "Cylinder 12 Exhaust Valve Control Circuit High", "P3497": "Cylinder Deactivation System", - "U0001" : "High Speed CAN Communication Bus" , - "U0002" : "High Speed CAN Communication Bus (Performance)" , - "U0003" : "High Speed CAN Communication Bus (Open)" , - "U0004" : "High Speed CAN Communication Bus (Low)" , - "U0005" : "High Speed CAN Communication Bus (High)" , - "U0006" : "High Speed CAN Communication Bus (Open)" , - "U0007" : "High Speed CAN Communication Bus (Low)" , - "U0008" : "High Speed CAN Communication Bus (High)" , - "U0009" : "High Speed CAN Communication Bus (shorted to Bus)" , - "U0010" : "Medium Speed CAN Communication Bus" , - "U0011" : "Medium Speed CAN Communication Bus (Performance)" , - "U0012" : "Medium Speed CAN Communication Bus (Open)" , - "U0013" : "Medium Speed CAN Communication Bus (Low)" , - "U0014" : "Medium Speed CAN Communication Bus (High)" , - "U0015" : "Medium Speed CAN Communication Bus (Open)" , - "U0016" : "Medium Speed CAN Communication Bus (Low)" , - "U0017" : "Medium Speed CAN Communication Bus (High)" , - "U0018" : "Medium Speed CAN Communication Bus (shorted to Bus)" , - "U0019" : "Low Speed CAN Communication Bus" , - "U0020" : "Low Speed CAN Communication Bus (Performance)" , - "U0021" : "Low Speed CAN Communication Bus (Open)" , - "U0022" : "Low Speed CAN Communication Bus (Low)" , - "U0023" : "Low Speed CAN Communication Bus (High)" , - "U0024" : "Low Speed CAN Communication Bus (Open)" , - "U0025" : "Low Speed CAN Communication Bus (Low)" , - "U0026" : "Low Speed CAN Communication Bus (High)" , - "U0027" : "Low Speed CAN Communication Bus (shorted to Bus)" , - "U0028" : "Vehicle Communication Bus A" , - "U0029" : "Vehicle Communication Bus A (Performance)" , - "U0030" : "Vehicle Communication Bus A (Open)" , - "U0031" : "Vehicle Communication Bus A (Low)" , - "U0032" : "Vehicle Communication Bus A (High)" , - "U0033" : "Vehicle Communication Bus A (Open)" , - "U0034" : "Vehicle Communication Bus A (Low)" , - "U0035" : "Vehicle Communication Bus A (High)" , - "U0036" : "Vehicle Communication Bus A (shorted to Bus A)" , - "U0037" : "Vehicle Communication Bus B" , - "U0038" : "Vehicle Communication Bus B (Performance)" , - "U0039" : "Vehicle Communication Bus B (Open)" , - "U0040" : "Vehicle Communication Bus B (Low)" , - "U0041" : "Vehicle Communication Bus B (High)" , - "U0042" : "Vehicle Communication Bus B (Open)" , - "U0043" : "Vehicle Communication Bus B (Low)" , - "U0044" : "Vehicle Communication Bus B (High)" , - "U0045" : "Vehicle Communication Bus B (shorted to Bus B)" , - "U0046" : "Vehicle Communication Bus C" , - "U0047" : "Vehicle Communication Bus C (Performance)" , - "U0048" : "Vehicle Communication Bus C (Open)" , - "U0049" : "Vehicle Communication Bus C (Low)" , - "U0050" : "Vehicle Communication Bus C (High)" , - "U0051" : "Vehicle Communication Bus C (Open)" , - "U0052" : "Vehicle Communication Bus C (Low)" , - "U0053" : "Vehicle Communication Bus C (High)" , - "U0054" : "Vehicle Communication Bus C (shorted to Bus C)" , - "U0055" : "Vehicle Communication Bus D" , - "U0056" : "Vehicle Communication Bus D (Performance)" , - "U0057" : "Vehicle Communication Bus D (Open)" , - "U0058" : "Vehicle Communication Bus D (Low)" , - "U0059" : "Vehicle Communication Bus D (High)" , - "U0060" : "Vehicle Communication Bus D (Open)" , - "U0061" : "Vehicle Communication Bus D (Low)" , - "U0062" : "Vehicle Communication Bus D (High)" , - "U0063" : "Vehicle Communication Bus D (shorted to Bus D)" , - "U0064" : "Vehicle Communication Bus E" , - "U0065" : "Vehicle Communication Bus E (Performance)" , - "U0066" : "Vehicle Communication Bus E (Open)" , - "U0067" : "Vehicle Communication Bus E (Low)" , - "U0068" : "Vehicle Communication Bus E (High)" , - "U0069" : "Vehicle Communication Bus E (Open)" , - "U0070" : "Vehicle Communication Bus E (Low)" , - "U0071" : "Vehicle Communication Bus E (High)" , - "U0072" : "Vehicle Communication Bus E (shorted to Bus E)" , - "U0073" : "Control Module Communication Bus Off" , - "U0074" : "Reserved by J2012" , - "U0075" : "Reserved by J2012" , - "U0076" : "Reserved by J2012" , - "U0077" : "Reserved by J2012" , - "U0078" : "Reserved by J2012" , - "U0079" : "Reserved by J2012" , - "U0080" : "Reserved by J2012" , - "U0081" : "Reserved by J2012" , - "U0082" : "Reserved by J2012" , - "U0083" : "Reserved by J2012" , - "U0084" : "Reserved by J2012" , - "U0085" : "Reserved by J2012" , - "U0086" : "Reserved by J2012" , - "U0087" : "Reserved by J2012" , - "U0088" : "Reserved by J2012" , - "U0089" : "Reserved by J2012" , - "U0090" : "Reserved by J2012" , - "U0091" : "Reserved by J2012" , - "U0092" : "Reserved by J2012" , - "U0093" : "Reserved by J2012" , - "U0094" : "Reserved by J2012" , - "U0095" : "Reserved by J2012" , - "U0096" : "Reserved by J2012" , - "U0097" : "Reserved by J2012" , - "U0098" : "Reserved by J2012" , - "U0099" : "Reserved by J2012" , - "U0100" : "Lost Communication With ECM/PCM A" , - "U0101" : "Lost Communication with TCM" , - "U0102" : "Lost Communication with Transfer Case Control Module" , - "U0103" : "Lost Communication With Gear Shift Module" , - "U0104" : "Lost Communication With Cruise Control Module" , - "U0105" : "Lost Communication With Fuel Injector Control Module" , - "U0106" : "Lost Communication With Glow Plug Control Module" , - "U0107" : "Lost Communication With Throttle Actuator Control Module" , - "U0108" : "Lost Communication With Alternative Fuel Control Module" , - "U0109" : "Lost Communication With Fuel Pump Control Module" , - "U0110" : "Lost Communication With Drive Motor Control Module" , - "U0111" : "Lost Communication With Battery Energy Control Module 'A'" , - "U0112" : "Lost Communication With Battery Energy Control Module 'B'" , - "U0113" : "Lost Communication With Emissions Critical Control Information" , - "U0114" : "Lost Communication With Four-Wheel Drive Clutch Control Module" , - "U0115" : "Lost Communication With ECM/PCM B" , - "U0116" : "Reserved by J2012" , - "U0117" : "Reserved by J2012" , - "U0118" : "Reserved by J2012" , - "U0119" : "Reserved by J2012" , - "U0120" : "Reserved by J2012" , - "U0121" : "Lost Communication With Anti-Lock Brake System (ABS) Control Module" , - "U0122" : "Lost Communication With Vehicle Dynamics Control Module" , - "U0123" : "Lost Communication With Yaw Rate Sensor Module" , - "U0124" : "Lost Communication With Lateral Acceleration Sensor Module" , - "U0125" : "Lost Communication With Multi-axis Acceleration Sensor Module" , - "U0126" : "Lost Communication With Steering Angle Sensor Module" , - "U0127" : "Lost Communication With Tire Pressure Monitor Module" , - "U0128" : "Lost Communication With Park Brake Control Module" , - "U0129" : "Lost Communication With Brake System Control Module" , - "U0130" : "Lost Communication With Steering Effort Control Module" , - "U0131" : "Lost Communication With Power Steering Control Module" , - "U0132" : "Lost Communication With Ride Level Control Module" , - "U0133" : "Reserved by J2012" , - "U0134" : "Reserved by J2012" , - "U0135" : "Reserved by J2012" , - "U0136" : "Reserved by J2012" , - "U0137" : "Reserved by J2012" , - "U0138" : "Reserved by J2012" , - "U0139" : "Reserved by J2012" , - "U0140" : "Lost Communication With Body Control Module" , - "U0141" : "Lost Communication With Body Control Module 'A'" , - "U0142" : "Lost Communication With Body Control Module 'B'" , - "U0143" : "Lost Communication With Body Control Module 'C'" , - "U0144" : "Lost Communication With Body Control Module 'D'" , - "U0145" : "Lost Communication With Body Control Module 'E'" , - "U0146" : "Lost Communication With Gateway 'A'" , - "U0147" : "Lost Communication With Gateway 'B'" , - "U0148" : "Lost Communication With Gateway 'C'" , - "U0149" : "Lost Communication With Gateway 'D'" , - "U0150" : "Lost Communication With Gateway 'E'" , - "U0151" : "Lost Communication With Restraints Control Module" , - "U0152" : "Lost Communication With Side Restraints Control Module Left" , - "U0153" : "Lost Communication With Side Restraints Control Module Right" , - "U0154" : "Lost Communication With Restraints Occupant Sensing Control Module" , - "U0155" : "Lost Communication With Instrument Panel Cluster (IPC) Control Module" , - "U0156" : "Lost Communication With Information Center 'A'" , - "U0157" : "Lost Communication With Information Center 'B'" , - "U0158" : "Lost Communication With Head Up Display" , - "U0159" : "Lost Communication With Parking Assist Control Module" , - "U0160" : "Lost Communication With Audible Alert Control Module" , - "U0161" : "Lost Communication With Compass Module" , - "U0162" : "Lost Communication With Navigation Display Module" , - "U0163" : "Lost Communication With Navigation Control Module" , - "U0164" : "Lost Communication With HVAC Control Module" , - "U0165" : "Lost Communication With HVAC Control Module Rear" , - "U0166" : "Lost Communication With Auxiliary Heater Control Module" , - "U0167" : "Lost Communication With Vehicle Immobilizer Control Module" , - "U0168" : "Lost Communication With Vehicle Security Control Module" , - "U0169" : "Lost Communication With Sunroof Control Module" , - "U0170" : "Lost Communication With 'Restraints System Sensor A'" , - "U0171" : "Lost Communication With 'Restraints System Sensor B'" , - "U0172" : "Lost Communication With 'Restraints System Sensor C'" , - "U0173" : "Lost Communication With 'Restraints System Sensor D'" , - "U0174" : "Lost Communication With 'Restraints System Sensor E'" , - "U0175" : "Lost Communication With 'Restraints System Sensor F'" , - "U0176" : "Lost Communication With 'Restraints System Sensor G'" , - "U0177" : "Lost Communication With 'Restraints System Sensor H'" , - "U0178" : "Lost Communication With 'Restraints System Sensor I'" , - "U0179" : "Lost Communication With 'Restraints System Sensor J'" , - "U0180" : "Lost Communication With Automatic Lighting Control Module" , - "U0181" : "Lost Communication With Headlamp Leveling Control Module" , - "U0182" : "Lost Communication With Lighting Control Module Front" , - "U0183" : "Lost Communication With Lighting Control Module Rear" , - "U0184" : "Lost Communication With Radio" , - "U0185" : "Lost Communication With Antenna Control Module" , - "U0186" : "Lost Communication With Audio Amplifier" , - "U0187" : "Lost Communication With Digital Disc Player/Changer Module 'A'" , - "U0188" : "Lost Communication With Digital Disc Player/Changer Module 'B'" , - "U0189" : "Lost Communication With Digital Disc Player/Changer Module 'C'" , - "U0190" : "Lost Communication With Digital Disc Player/Changer Module 'D'" , - "U0191" : "Lost Communication With Television" , - "U0192" : "Lost Communication With Personal Computer" , - "U0193" : "Lost Communication With 'Digital Audio Control Module A'" , - "U0194" : "Lost Communication With 'Digital Audio Control Module B'" , - "U0195" : "Lost Communication With Subscription Entertainment Receiver Module" , - "U0196" : "Lost Communication With Rear Seat Entertainment Control Module" , - "U0197" : "Lost Communication With Telephone Control Module" , - "U0198" : "Lost Communication With Telematic Control Module" , - "U0199" : "Lost Communication With 'Door Control Module A'" , - "U0200" : "Lost Communication With 'Door Control Module B'" , - "U0201" : "Lost Communication With 'Door Control Module C'" , - "U0202" : "Lost Communication With 'Door Control Module D'" , - "U0203" : "Lost Communication With 'Door Control Module E'" , - "U0204" : "Lost Communication With 'Door Control Module F'" , - "U0205" : "Lost Communication With 'Door Control Module G'" , - "U0206" : "Lost Communication With Folding Top Control Module" , - "U0207" : "Lost Communication With Moveable Roof Control Module" , - "U0208" : "Lost Communication With 'Seat Control Module A'" , - "U0209" : "Lost Communication With 'Seat Control Module B'" , - "U0210" : "Lost Communication With 'Seat Control Module C'" , - "U0211" : "Lost Communication With 'Seat Control Module D'" , - "U0212" : "Lost Communication With Steering Column Control Module" , - "U0213" : "Lost Communication With Mirror Control Module" , - "U0214" : "Lost Communication With Remote Function Actuation" , - "U0215" : "Lost Communication With 'Door Switch A'" , - "U0216" : "Lost Communication With 'Door Switch B'" , - "U0217" : "Lost Communication With 'Door Switch C'" , - "U0218" : "Lost Communication With 'Door Switch D'" , - "U0219" : "Lost Communication With 'Door Switch E'" , - "U0220" : "Lost Communication With 'Door Switch F'" , - "U0221" : "Lost Communication With 'Door Switch G'" , - "U0222" : "Lost Communication With 'Door Window Motor A'" , - "U0223" : "Lost Communication With 'Door Window Motor B'" , - "U0224" : "Lost Communication With 'Door Window Motor C'" , - "U0225" : "Lost Communication With 'Door Window Motor D'" , - "U0226" : "Lost Communication With 'Door Window Motor E'" , - "U0227" : "Lost Communication With 'Door Window Motor F'" , - "U0228" : "Lost Communication With 'Door Window Motor G'" , - "U0229" : "Lost Communication With Heated Steering Wheel Module" , - "U0230" : "Lost Communication With Rear Gate Module" , - "U0231" : "Lost Communication With Rain Sensing Module" , - "U0232" : "Lost Communication With Side Obstacle Detection Control Module Left" , - "U0233" : "Lost Communication With Side Obstacle Detection Control Module Right" , - "U0234" : "Lost Communication With Convenience Recall Module" , - "U0235" : "Lost Communication With Cruise Control Front Distance Range Sensor" , - "U0300" : "Internal Control Module Software Incompatibility" , - "U0301" : "Software Incompatibility with ECM/PCM" , - "U0302" : "Software Incompatibility with Transmission Control Module" , - "U0303" : "Software Incompatibility with Transfer Case Control Module" , - "U0304" : "Software Incompatibility with Gear Shift Control Module" , - "U0305" : "Software Incompatibility with Cruise Control Module" , - "U0306" : "Software Incompatibility with Fuel Injector Control Module" , - "U0307" : "Software Incompatibility with Glow Plug Control Module" , - "U0308" : "Software Incompatibility with Throttle Actuator Control Module" , - "U0309" : "Software Incompatibility with Alternative Fuel Control Module" , - "U0310" : "Software Incompatibility with Fuel Pump Control Module" , - "U0311" : "Software Incompatibility with Drive Motor Control Module" , - "U0312" : "Software Incompatibility with Battery Energy Control Module A" , - "U0313" : "Software Incompatibility with Battery Energy Control Module B" , - "U0314" : "Software Incompatibility with Four-Wheel Drive Clutch Control Module" , - "U0315" : "Software Incompatibility with Anti-Lock Brake System Control Module" , - "U0316" : "Software Incompatibility with Vehicle Dynamics Control Module" , - "U0317" : "Software Incompatibility with Park Brake Control Module" , - "U0318" : "Software Incompatibility with Brake System Control Module" , - "U0319" : "Software Incompatibility with Steering Effort Control Module" , - "U0320" : "Software Incompatibility with Power Steering Control Module" , - "U0321" : "Software Incompatibility with Ride Level Control Module" , - "U0322" : "Software Incompatibility with Body Control Module" , - "U0323" : "Software Incompatibility with Instrument Panel Control Module" , - "U0324" : "Software Incompatibility with HVAC Control Module" , - "U0325" : "Software Incompatibility with Auxiliary Heater Control Module" , - "U0326" : "Software Incompatibility with Vehicle Immobilizer Control Module" , - "U0327" : "Software Incompatibility with Vehicle Security Control Module" , - "U0328" : "Software Incompatibility with Steering Angle Sensor Module" , - "U0329" : "Software Incompatibility with Steering Column Control Module" , - "U0330" : "Software Incompatibility with Tire Pressure Monitor Module" , - "U0331" : "Software Incompatibility with Body Control Module 'A'" , - "U0400" : "Invalid Data Received" , - "U0401" : "Invalid Data Received From ECM/PCM" , - "U0402" : "Invalid Data Received From Transmission Control Module" , - "U0403" : "Invalid Data Received From Transfer Case Control Module" , - "U0404" : "Invalid Data Received From Gear Shift Control Module" , - "U0405" : "Invalid Data Received From Cruise Control Module" , - "U0406" : "Invalid Data Received From Fuel Injector Control Module" , - "U0407" : "Invalid Data Received From Glow Plug Control Module" , - "U0408" : "Invalid Data Received From Throttle Actuator Control Module" , - "U0409" : "Invalid Data Received From Alternative Fuel Control Module" , - "U0410" : "Invalid Data Received From Fuel Pump Control Module" , - "U0411" : "Invalid Data Received From Drive Motor Control Module" , - "U0412" : "Invalid Data Received From Battery Energy Control Module A" , - "U0413" : "Invalid Data Received From Battery Energy Control Module B" , - "U0414" : "Invalid Data Received From Four-Wheel Drive Clutch Control Module" , - "U0415" : "Invalid Data Received From Anti-Lock Brake System Control Module" , - "U0416" : "Invalid Data Received From Vehicle Dynamics Control Module" , - "U0417" : "Invalid Data Received From Park Brake Control Module" , - "U0418" : "Invalid Data Received From Brake System Control Module" , - "U0419" : "Invalid Data Received From Steering Effort Control Module" , - "U0420" : "Invalid Data Received From Power Steering Control Module" , - "U0421" : "Invalid Data Received From Ride Level Control Module" , - "U0422" : "Invalid Data Received From Body Control Module" , - "U0423" : "Invalid Data Received From Instrument Panel Control Module" , - "U0424" : "Invalid Data Received From HVAC Control Module" , - "U0425" : "Invalid Data Received From Auxiliary Heater Control Module" , - "U0426" : "Invalid Data Received From Vehicle Immobilizer Control Module" , - "U0427" : "Invalid Data Received From Vehicle Security Control Module" , - "U0428" : "Invalid Data Received From Steering Angle Sensor Module" , - "U0429" : "Invalid Data Received From Steering Column Control Module" , - "U0430" : "Invalid Data Received From Tire Pressure Monitor Module" , - "U0431" : "Invalid Data Received From Body Control Module 'A'" + + "U0001" : "High Speed CAN Communication Bus", + "U0002" : "High Speed CAN Communication Bus (Performance)", + "U0003" : "High Speed CAN Communication Bus (Open)", + "U0004" : "High Speed CAN Communication Bus (Low)", + "U0005" : "High Speed CAN Communication Bus (High)", + "U0006" : "High Speed CAN Communication Bus (Open)", + "U0007" : "High Speed CAN Communication Bus (Low)", + "U0008" : "High Speed CAN Communication Bus (High)", + "U0009" : "High Speed CAN Communication Bus (shorted to Bus)", + "U0010" : "Medium Speed CAN Communication Bus", + "U0011" : "Medium Speed CAN Communication Bus (Performance)", + "U0012" : "Medium Speed CAN Communication Bus (Open)", + "U0013" : "Medium Speed CAN Communication Bus (Low)", + "U0014" : "Medium Speed CAN Communication Bus (High)", + "U0015" : "Medium Speed CAN Communication Bus (Open)", + "U0016" : "Medium Speed CAN Communication Bus (Low)", + "U0017" : "Medium Speed CAN Communication Bus (High)", + "U0018" : "Medium Speed CAN Communication Bus (shorted to Bus)", + "U0019" : "Low Speed CAN Communication Bus", + "U0020" : "Low Speed CAN Communication Bus (Performance)", + "U0021" : "Low Speed CAN Communication Bus (Open)", + "U0022" : "Low Speed CAN Communication Bus (Low)", + "U0023" : "Low Speed CAN Communication Bus (High)", + "U0024" : "Low Speed CAN Communication Bus (Open)", + "U0025" : "Low Speed CAN Communication Bus (Low)", + "U0026" : "Low Speed CAN Communication Bus (High)", + "U0027" : "Low Speed CAN Communication Bus (shorted to Bus)", + "U0028" : "Vehicle Communication Bus A", + "U0029" : "Vehicle Communication Bus A (Performance)", + "U0030" : "Vehicle Communication Bus A (Open)", + "U0031" : "Vehicle Communication Bus A (Low)", + "U0032" : "Vehicle Communication Bus A (High)", + "U0033" : "Vehicle Communication Bus A (Open)", + "U0034" : "Vehicle Communication Bus A (Low)", + "U0035" : "Vehicle Communication Bus A (High)", + "U0036" : "Vehicle Communication Bus A (shorted to Bus A)", + "U0037" : "Vehicle Communication Bus B", + "U0038" : "Vehicle Communication Bus B (Performance)", + "U0039" : "Vehicle Communication Bus B (Open)", + "U0040" : "Vehicle Communication Bus B (Low)", + "U0041" : "Vehicle Communication Bus B (High)", + "U0042" : "Vehicle Communication Bus B (Open)", + "U0043" : "Vehicle Communication Bus B (Low)", + "U0044" : "Vehicle Communication Bus B (High)", + "U0045" : "Vehicle Communication Bus B (shorted to Bus B)", + "U0046" : "Vehicle Communication Bus C", + "U0047" : "Vehicle Communication Bus C (Performance)", + "U0048" : "Vehicle Communication Bus C (Open)", + "U0049" : "Vehicle Communication Bus C (Low)", + "U0050" : "Vehicle Communication Bus C (High)", + "U0051" : "Vehicle Communication Bus C (Open)", + "U0052" : "Vehicle Communication Bus C (Low)", + "U0053" : "Vehicle Communication Bus C (High)", + "U0054" : "Vehicle Communication Bus C (shorted to Bus C)", + "U0055" : "Vehicle Communication Bus D", + "U0056" : "Vehicle Communication Bus D (Performance)", + "U0057" : "Vehicle Communication Bus D (Open)", + "U0058" : "Vehicle Communication Bus D (Low)", + "U0059" : "Vehicle Communication Bus D (High)", + "U0060" : "Vehicle Communication Bus D (Open)", + "U0061" : "Vehicle Communication Bus D (Low)", + "U0062" : "Vehicle Communication Bus D (High)", + "U0063" : "Vehicle Communication Bus D (shorted to Bus D)", + "U0064" : "Vehicle Communication Bus E", + "U0065" : "Vehicle Communication Bus E (Performance)", + "U0066" : "Vehicle Communication Bus E (Open)", + "U0067" : "Vehicle Communication Bus E (Low)", + "U0068" : "Vehicle Communication Bus E (High)", + "U0069" : "Vehicle Communication Bus E (Open)", + "U0070" : "Vehicle Communication Bus E (Low)", + "U0071" : "Vehicle Communication Bus E (High)", + "U0072" : "Vehicle Communication Bus E (shorted to Bus E)", + "U0073" : "Control Module Communication Bus Off", + "U0074" : "Reserved by J2012", + "U0075" : "Reserved by J2012", + "U0076" : "Reserved by J2012", + "U0077" : "Reserved by J2012", + "U0078" : "Reserved by J2012", + "U0079" : "Reserved by J2012", + "U0080" : "Reserved by J2012", + "U0081" : "Reserved by J2012", + "U0082" : "Reserved by J2012", + "U0083" : "Reserved by J2012", + "U0084" : "Reserved by J2012", + "U0085" : "Reserved by J2012", + "U0086" : "Reserved by J2012", + "U0087" : "Reserved by J2012", + "U0088" : "Reserved by J2012", + "U0089" : "Reserved by J2012", + "U0090" : "Reserved by J2012", + "U0091" : "Reserved by J2012", + "U0092" : "Reserved by J2012", + "U0093" : "Reserved by J2012", + "U0094" : "Reserved by J2012", + "U0095" : "Reserved by J2012", + "U0096" : "Reserved by J2012", + "U0097" : "Reserved by J2012", + "U0098" : "Reserved by J2012", + "U0099" : "Reserved by J2012", + "U0100" : "Lost Communication With ECM/PCM A", + "U0101" : "Lost Communication with TCM", + "U0102" : "Lost Communication with Transfer Case Control Module", + "U0103" : "Lost Communication With Gear Shift Module", + "U0104" : "Lost Communication With Cruise Control Module", + "U0105" : "Lost Communication With Fuel Injector Control Module", + "U0106" : "Lost Communication With Glow Plug Control Module", + "U0107" : "Lost Communication With Throttle Actuator Control Module", + "U0108" : "Lost Communication With Alternative Fuel Control Module", + "U0109" : "Lost Communication With Fuel Pump Control Module", + "U0110" : "Lost Communication With Drive Motor Control Module", + "U0111" : "Lost Communication With Battery Energy Control Module 'A'", + "U0112" : "Lost Communication With Battery Energy Control Module 'B'", + "U0113" : "Lost Communication With Emissions Critical Control Information", + "U0114" : "Lost Communication With Four-Wheel Drive Clutch Control Module", + "U0115" : "Lost Communication With ECM/PCM B", + "U0116" : "Reserved by J2012", + "U0117" : "Reserved by J2012", + "U0118" : "Reserved by J2012", + "U0119" : "Reserved by J2012", + "U0120" : "Reserved by J2012", + "U0121" : "Lost Communication With Anti-Lock Brake System (ABS) Control Module", + "U0122" : "Lost Communication With Vehicle Dynamics Control Module", + "U0123" : "Lost Communication With Yaw Rate Sensor Module", + "U0124" : "Lost Communication With Lateral Acceleration Sensor Module", + "U0125" : "Lost Communication With Multi-axis Acceleration Sensor Module", + "U0126" : "Lost Communication With Steering Angle Sensor Module", + "U0127" : "Lost Communication With Tire Pressure Monitor Module", + "U0128" : "Lost Communication With Park Brake Control Module", + "U0129" : "Lost Communication With Brake System Control Module", + "U0130" : "Lost Communication With Steering Effort Control Module", + "U0131" : "Lost Communication With Power Steering Control Module", + "U0132" : "Lost Communication With Ride Level Control Module", + "U0133" : "Reserved by J2012", + "U0134" : "Reserved by J2012", + "U0135" : "Reserved by J2012", + "U0136" : "Reserved by J2012", + "U0137" : "Reserved by J2012", + "U0138" : "Reserved by J2012", + "U0139" : "Reserved by J2012", + "U0140" : "Lost Communication With Body Control Module", + "U0141" : "Lost Communication With Body Control Module 'A'", + "U0142" : "Lost Communication With Body Control Module 'B'", + "U0143" : "Lost Communication With Body Control Module 'C'", + "U0144" : "Lost Communication With Body Control Module 'D'", + "U0145" : "Lost Communication With Body Control Module 'E'", + "U0146" : "Lost Communication With Gateway 'A'", + "U0147" : "Lost Communication With Gateway 'B'", + "U0148" : "Lost Communication With Gateway 'C'", + "U0149" : "Lost Communication With Gateway 'D'", + "U0150" : "Lost Communication With Gateway 'E'", + "U0151" : "Lost Communication With Restraints Control Module", + "U0152" : "Lost Communication With Side Restraints Control Module Left", + "U0153" : "Lost Communication With Side Restraints Control Module Right", + "U0154" : "Lost Communication With Restraints Occupant Sensing Control Module", + "U0155" : "Lost Communication With Instrument Panel Cluster (IPC) Control Module", + "U0156" : "Lost Communication With Information Center 'A'", + "U0157" : "Lost Communication With Information Center 'B'", + "U0158" : "Lost Communication With Head Up Display", + "U0159" : "Lost Communication With Parking Assist Control Module", + "U0160" : "Lost Communication With Audible Alert Control Module", + "U0161" : "Lost Communication With Compass Module", + "U0162" : "Lost Communication With Navigation Display Module", + "U0163" : "Lost Communication With Navigation Control Module", + "U0164" : "Lost Communication With HVAC Control Module", + "U0165" : "Lost Communication With HVAC Control Module Rear", + "U0166" : "Lost Communication With Auxiliary Heater Control Module", + "U0167" : "Lost Communication With Vehicle Immobilizer Control Module", + "U0168" : "Lost Communication With Vehicle Security Control Module", + "U0169" : "Lost Communication With Sunroof Control Module", + "U0170" : "Lost Communication With 'Restraints System Sensor A'", + "U0171" : "Lost Communication With 'Restraints System Sensor B'", + "U0172" : "Lost Communication With 'Restraints System Sensor C'", + "U0173" : "Lost Communication With 'Restraints System Sensor D'", + "U0174" : "Lost Communication With 'Restraints System Sensor E'", + "U0175" : "Lost Communication With 'Restraints System Sensor F'", + "U0176" : "Lost Communication With 'Restraints System Sensor G'", + "U0177" : "Lost Communication With 'Restraints System Sensor H'", + "U0178" : "Lost Communication With 'Restraints System Sensor I'", + "U0179" : "Lost Communication With 'Restraints System Sensor J'", + "U0180" : "Lost Communication With Automatic Lighting Control Module", + "U0181" : "Lost Communication With Headlamp Leveling Control Module", + "U0182" : "Lost Communication With Lighting Control Module Front", + "U0183" : "Lost Communication With Lighting Control Module Rear", + "U0184" : "Lost Communication With Radio", + "U0185" : "Lost Communication With Antenna Control Module", + "U0186" : "Lost Communication With Audio Amplifier", + "U0187" : "Lost Communication With Digital Disc Player/Changer Module 'A'", + "U0188" : "Lost Communication With Digital Disc Player/Changer Module 'B'", + "U0189" : "Lost Communication With Digital Disc Player/Changer Module 'C'", + "U0190" : "Lost Communication With Digital Disc Player/Changer Module 'D'", + "U0191" : "Lost Communication With Television", + "U0192" : "Lost Communication With Personal Computer", + "U0193" : "Lost Communication With 'Digital Audio Control Module A'", + "U0194" : "Lost Communication With 'Digital Audio Control Module B'", + "U0195" : "Lost Communication With Subscription Entertainment Receiver Module", + "U0196" : "Lost Communication With Rear Seat Entertainment Control Module", + "U0197" : "Lost Communication With Telephone Control Module", + "U0198" : "Lost Communication With Telematic Control Module", + "U0199" : "Lost Communication With 'Door Control Module A'", + "U0200" : "Lost Communication With 'Door Control Module B'", + "U0201" : "Lost Communication With 'Door Control Module C'", + "U0202" : "Lost Communication With 'Door Control Module D'", + "U0203" : "Lost Communication With 'Door Control Module E'", + "U0204" : "Lost Communication With 'Door Control Module F'", + "U0205" : "Lost Communication With 'Door Control Module G'", + "U0206" : "Lost Communication With Folding Top Control Module", + "U0207" : "Lost Communication With Moveable Roof Control Module", + "U0208" : "Lost Communication With 'Seat Control Module A'", + "U0209" : "Lost Communication With 'Seat Control Module B'", + "U0210" : "Lost Communication With 'Seat Control Module C'", + "U0211" : "Lost Communication With 'Seat Control Module D'", + "U0212" : "Lost Communication With Steering Column Control Module", + "U0213" : "Lost Communication With Mirror Control Module", + "U0214" : "Lost Communication With Remote Function Actuation", + "U0215" : "Lost Communication With 'Door Switch A'", + "U0216" : "Lost Communication With 'Door Switch B'", + "U0217" : "Lost Communication With 'Door Switch C'", + "U0218" : "Lost Communication With 'Door Switch D'", + "U0219" : "Lost Communication With 'Door Switch E'", + "U0220" : "Lost Communication With 'Door Switch F'", + "U0221" : "Lost Communication With 'Door Switch G'", + "U0222" : "Lost Communication With 'Door Window Motor A'", + "U0223" : "Lost Communication With 'Door Window Motor B'", + "U0224" : "Lost Communication With 'Door Window Motor C'", + "U0225" : "Lost Communication With 'Door Window Motor D'", + "U0226" : "Lost Communication With 'Door Window Motor E'", + "U0227" : "Lost Communication With 'Door Window Motor F'", + "U0228" : "Lost Communication With 'Door Window Motor G'", + "U0229" : "Lost Communication With Heated Steering Wheel Module", + "U0230" : "Lost Communication With Rear Gate Module", + "U0231" : "Lost Communication With Rain Sensing Module", + "U0232" : "Lost Communication With Side Obstacle Detection Control Module Left", + "U0233" : "Lost Communication With Side Obstacle Detection Control Module Right", + "U0234" : "Lost Communication With Convenience Recall Module", + "U0235" : "Lost Communication With Cruise Control Front Distance Range Sensor", + "U0300" : "Internal Control Module Software Incompatibility", + "U0301" : "Software Incompatibility with ECM/PCM", + "U0302" : "Software Incompatibility with Transmission Control Module", + "U0303" : "Software Incompatibility with Transfer Case Control Module", + "U0304" : "Software Incompatibility with Gear Shift Control Module", + "U0305" : "Software Incompatibility with Cruise Control Module", + "U0306" : "Software Incompatibility with Fuel Injector Control Module", + "U0307" : "Software Incompatibility with Glow Plug Control Module", + "U0308" : "Software Incompatibility with Throttle Actuator Control Module", + "U0309" : "Software Incompatibility with Alternative Fuel Control Module", + "U0310" : "Software Incompatibility with Fuel Pump Control Module", + "U0311" : "Software Incompatibility with Drive Motor Control Module", + "U0312" : "Software Incompatibility with Battery Energy Control Module A", + "U0313" : "Software Incompatibility with Battery Energy Control Module B", + "U0314" : "Software Incompatibility with Four-Wheel Drive Clutch Control Module", + "U0315" : "Software Incompatibility with Anti-Lock Brake System Control Module", + "U0316" : "Software Incompatibility with Vehicle Dynamics Control Module", + "U0317" : "Software Incompatibility with Park Brake Control Module", + "U0318" : "Software Incompatibility with Brake System Control Module", + "U0319" : "Software Incompatibility with Steering Effort Control Module", + "U0320" : "Software Incompatibility with Power Steering Control Module", + "U0321" : "Software Incompatibility with Ride Level Control Module", + "U0322" : "Software Incompatibility with Body Control Module", + "U0323" : "Software Incompatibility with Instrument Panel Control Module", + "U0324" : "Software Incompatibility with HVAC Control Module", + "U0325" : "Software Incompatibility with Auxiliary Heater Control Module", + "U0326" : "Software Incompatibility with Vehicle Immobilizer Control Module", + "U0327" : "Software Incompatibility with Vehicle Security Control Module", + "U0328" : "Software Incompatibility with Steering Angle Sensor Module", + "U0329" : "Software Incompatibility with Steering Column Control Module", + "U0330" : "Software Incompatibility with Tire Pressure Monitor Module", + "U0331" : "Software Incompatibility with Body Control Module 'A'", + "U0400" : "Invalid Data Received", + "U0401" : "Invalid Data Received From ECM/PCM", + "U0402" : "Invalid Data Received From Transmission Control Module", + "U0403" : "Invalid Data Received From Transfer Case Control Module", + "U0404" : "Invalid Data Received From Gear Shift Control Module", + "U0405" : "Invalid Data Received From Cruise Control Module", + "U0406" : "Invalid Data Received From Fuel Injector Control Module", + "U0407" : "Invalid Data Received From Glow Plug Control Module", + "U0408" : "Invalid Data Received From Throttle Actuator Control Module", + "U0409" : "Invalid Data Received From Alternative Fuel Control Module", + "U0410" : "Invalid Data Received From Fuel Pump Control Module", + "U0411" : "Invalid Data Received From Drive Motor Control Module", + "U0412" : "Invalid Data Received From Battery Energy Control Module A", + "U0413" : "Invalid Data Received From Battery Energy Control Module B", + "U0414" : "Invalid Data Received From Four-Wheel Drive Clutch Control Module", + "U0415" : "Invalid Data Received From Anti-Lock Brake System Control Module", + "U0416" : "Invalid Data Received From Vehicle Dynamics Control Module", + "U0417" : "Invalid Data Received From Park Brake Control Module", + "U0418" : "Invalid Data Received From Brake System Control Module", + "U0419" : "Invalid Data Received From Steering Effort Control Module", + "U0420" : "Invalid Data Received From Power Steering Control Module", + "U0421" : "Invalid Data Received From Ride Level Control Module", + "U0422" : "Invalid Data Received From Body Control Module", + "U0423" : "Invalid Data Received From Instrument Panel Control Module", + "U0424" : "Invalid Data Received From HVAC Control Module", + "U0425" : "Invalid Data Received From Auxiliary Heater Control Module", + "U0426" : "Invalid Data Received From Vehicle Immobilizer Control Module", + "U0427" : "Invalid Data Received From Vehicle Security Control Module", + "U0428" : "Invalid Data Received From Steering Angle Sensor Module", + "U0429" : "Invalid Data Received From Steering Column Control Module", + "U0430" : "Invalid Data Received From Tire Pressure Monitor Module", + "U0431" : "Invalid Data Received From Body Control Module 'A'", } +IGNITION_TYPE = [ + "Spark", + "Compression" +] + +SPARK_TESTS = [ + "EGR System", + "Oxygen Sensor Heater", + "Oxygen Sensor", + "A/C Refrigerant", + "Secondary Air System", + "Evaporative System", + "Heated Catalyst", + "Catalyst", +] + +COMPRESSION_TESTS = [ + "EGR and/or VVT System", + "PM filter monitoring", + "Exhaust Gas Sensor", + None, + "Boost Pressure", + None, + "NOx/SCR Monitor", + "NMHC Catalyst", +] + +FUEL_STATUS = [ + "Open loop due to insufficient engine temperature", + "Closed loop, using oxygen sensor feedback to determine fuel mix", + "Open loop due to engine load OR fuel cut due to deceleration", + "Open loop due to system failure", + "Closed loop, using at least one oxygen sensor but there is a fault in the feedback system", +] + +AIR_STATUS = [ + "Upstream" + "Downstream of catalytic converter" + "From the outside atmosphere or off" + "Pump commanded on for diagnostics" +] + +OBD_COMPLIANCE = [ + "Undefined" + "OBD-II as defined by the CARB" + "OBD as defined by the EPA" + "OBD and OBD-II" + "OBD-I" + "Not OBD compliant" + "EOBD (Europe)" + "EOBD and OBD-II" + "EOBD and OBD" + "EOBD, OBD and OBD II" + "JOBD (Japan)" + "JOBD and OBD II" + "JOBD and EOBD" + "JOBD, EOBD, and OBD II" + "Reserved" + "Reserved" + "Reserved" + "Engine Manufacturer Diagnostics (EMD)" + "Engine Manufacturer Diagnostics Enhanced (EMD+)" + "Heavy Duty On-Board Diagnostics (Child/Partial) (HD OBD-C)" + "Heavy Duty On-Board Diagnostics (HD OBD)" + "World Wide Harmonized OBD (WWH OBD)" + "Reserved" + "Heavy Duty Euro OBD Stage I without NOx control (HD EOBD-I)" + "Heavy Duty Euro OBD Stage I with NOx control (HD EOBD-I N)" + "Heavy Duty Euro OBD Stage II without NOx control (HD EOBD-II)" + "Heavy Duty Euro OBD Stage II with NOx control (HD EOBD-II N)" + "Reserved" + "Brazil OBD Phase 1 (OBDBr-1)" + "Brazil OBD Phase 2 (OBDBr-2)" + "Korean OBD (KOBD)" + "India OBD I (IOBD I)" + "India OBD II (IOBD II)" + "Heavy Duty Euro OBD Stage VI (HD EOBD-IV)" +] -ptest= [ - "DTCs", - "MIL", - #A - "Misfire", - "Fuel system", - "Components", - #B,D - "Catalyst", - "Heated Catalyst", - "Evaporative system", - "Secondary Air System", - "A/C Refrigerant" , - "Oxygen Sensor", - "Oxygen Sensor Heater", - "EGR SystemC7" , +FUEL_TYPES = [ + "Not available" + "Gasoline" + "Methanol" + "Ethanol" + "Diesel" + "LPG" + "CNG" + "Propane" + "Electric" + "Bifuel running Gasoline" + "Bifuel running Methanol" + "Bifuel running Ethanol" + "Bifuel running LPG" + "Bifuel running CNG" + "Bifuel running Propane" + "Bifuel running Electricity" + "Bifuel running electric and combustion engine" + "Hybrid gasoline" + "Hybrid Ethanol" + "Hybrid Diesel" + "Hybrid Electric" + "Hybrid running electric and combustion engine" + "Hybrid Regenerative" + "Bifuel running diesel" ] diff --git a/obd/commands.py b/obd/commands.py index 4b95d01e..75249fc4 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -67,7 +67,7 @@ def compute(result): # mode 1 # sensor name description mode cmd bytes decoder OBDCommand("PIDS" , "Supported PIDs" , "01", "00" , 4, noop ), - OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01" , 4, noop ), + OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01" , 4, status ), OBDCommand("FREEZE_DTC" , "Freeze DTC" , "01", "02" , 2, noop ), OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03" , 2, noop ), OBDCommand("LOAD" , "Calculated Engine Load" , "01", "04" , 1, percent ), @@ -94,7 +94,7 @@ def compute(result): OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2" , "01", "19" , 2, sensor_voltage ), OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3" , "01", "1A" , 2, sensor_voltage ), OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4" , "01", "1B" , 2, sensor_voltage ), - OBDCommand("OBD_STANDARDS" , "OBD Standards Compliance" , "01", "1C" , 1, noop ), + OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "01", "1C" , 1, noop ), OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D" , 1, noop ), OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E" , 1, noop ), OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F" , 2, seconds ), diff --git a/obd/decoders.py b/obd/decoders.py index 6edea7cd..dfc26a11 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -1,13 +1,19 @@ +from utils import Value, Unit, Test, unhex, unbin, bitstring, bitToBool +from codes import * -from utils import Value, Unit, unhex, unbin, bitstring +def noop(_hex): + return _hex -# functions accepting hex responses from the OBD connection, and computing/returning values with units -def noop(_hex): - return Value(_hex, Unit.NONE) + + +''' +Sensor decoders +Return Value object with value and units +''' # 0 to 100 % def percent(_hex): @@ -72,7 +78,62 @@ def seconds(_hex): return Value(v, Unit.SECONDS) -# these functions draw data from the same PID + + + +''' +Special decoders +Return objects, lists, etc +''' + + + +def status(_hex): + bits = bitstring(_hex) + + output = {} + output["Check Engine Light"] = bitToBool(bits[0]) + output["DTC Count"] = unbin(bits[1:8]) + output["Ignition Type"] = IGNITION_TYPE[unbin(bits[12])] + output["Tests"] = [] + + output["Tests"].append(Test("Misfire", \ + bitToBool(bits[15]), \ + bitToBool(bits[11]))) + + output["Tests"].append(Test("Fuel System", \ + bitToBool(bits[16]), \ + bitToBool(bits[12]))) + + output["Tests"].append(Test("Components", \ + bitToBool(bits[17]), \ + bitToBool(bits[13]))) + + + # different tests for different ignition types + if(output["Ignition Type"] == IGNITION_TYPE[0]): # spark + for i in range(8): + if SPARK_TESTS[i] is not None: + + t = Test(SPARK_TESTS[i], \ + bitToBool(bits[(2 * 8) + i]), \ + bitToBool(bits[(3 * 8) + i])) + + output["Tests"].append(t) + + elif(output["Ignition Type"] == IGNITION_TYPE[1]): # compression + for i in range(8): + if COMPRESSION_TESTS[i] is not None: + + t = Test(COMPRESSION_TESTS[i], \ + bitToBool(bits[(2 * 8) + i]), \ + bitToBool(bits[(3 * 8) + i])) + + output["Tests"].append(t) + + return output + + def mil(_hex): v = bitstring(_hex) return v[0] == '1' @@ -81,6 +142,15 @@ def dtc_count(_hex): v = bitstring(_hex) return unbin(v[1:8]) + +# Get the description of a DTC +def describeCode(code): + code.upper() + if DTC.has_key(code): + return DTC[code] + else: + return "Unknown or manufacturer specific code. Consult the internet." + # converts 2 bytes of hex into a DTC code def dtc(_hex): dtc = "" diff --git a/obd/utils.py b/obd/utils.py index cf2edf58..7a03bc43 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -5,8 +5,6 @@ class Unit: NONE = None - BITSTRING = "Bit String" - DTC = "Diagnostic Trouble Code" PERCENT = "Percent" VOLT = "Volt" F = "F" @@ -30,6 +28,19 @@ def __str__(self): return "%s %s" % (str(self.value), str(self.unit)) +class Test(): + def __init__(self, name, available, incomplete): + self.name = name + self.available = available + self.incomplete = incomplete + + def __str__(self): + a = "Available" if self.available else "Unavailable" + c = "Incomplete" if self.incomplete else "Complete" + return "Test %s: %s, %s" % (name, a, c) + + + def unhex(_hex): return int(_hex, 16) @@ -39,6 +50,9 @@ def unbin(_bin): def bitstring(_hex): return bin(unhex(_hex))[2:] +def bitToBool(_bit): + return (_bit == '1') + def tryPort(portStr): """returns boolean for port availability""" From b291ef3b3dd21e79b6c4eca5f9f36ab72d307200 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 17 Oct 2014 23:03:39 -0400 Subject: [PATCH 035/569] added fuel, air, and compliance decoders --- obd/commands.py | 68 +++++++++++++++++++++++++------------------------ obd/decoders.py | 41 ++++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 39 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 75249fc4..04535ad5 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -66,39 +66,41 @@ def compute(result): __mode1__ = [ # mode 1 # sensor name description mode cmd bytes decoder - OBDCommand("PIDS" , "Supported PIDs" , "01", "00" , 4, noop ), - OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01" , 4, status ), - OBDCommand("FREEZE_DTC" , "Freeze DTC" , "01", "02" , 2, noop ), - OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03" , 2, noop ), - OBDCommand("LOAD" , "Calculated Engine Load" , "01", "04" , 1, percent ), - OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05" , 1, temp ), - OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06" , 1, percent_centered ), - OBDCommand("Long_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07" , 1, percent_centered ), - OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08" , 1, percent_centered ), - OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09" , 1, percent_centered ), - OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A" , 1, fuel_pressure ), - OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "01", "0B" , 1, intake_pressure ), - OBDCommand("RPM" , "Engine RPM" , "01", "0C" , 2, rpm ), - OBDCommand("SPEED" , "Vehicle Speed" , "01", "0D" , 1, speed ), - OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E" , 1, timing_advance ), - OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F" , 1, temp ), - OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10" , 2, maf ), - OBDCommand("THROTTLE" , "Throttle Position" , "01", "11" , 1, percent ), - OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12" , 1, noop ), - OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13" , 1, noop ), - OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1" , "01", "14" , 2, sensor_voltage ), - OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2" , "01", "15" , 2, sensor_voltage ), - OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3" , "01", "16" , 2, sensor_voltage ), - OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4" , "01", "17" , 2, sensor_voltage ), - OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1" , "01", "18" , 2, sensor_voltage ), - OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2" , "01", "19" , 2, sensor_voltage ), - OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3" , "01", "1A" , 2, sensor_voltage ), - OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4" , "01", "1B" , 2, sensor_voltage ), - OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "01", "1C" , 1, noop ), - OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D" , 1, noop ), - OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E" , 1, noop ), - OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F" , 2, seconds ), - #OBDCommand("RUN_TIME_MIL" , "Engine Run Time MIL" , "01", "4D" , sec_to_min ), + OBDCommand("PIDS" , "Supported PIDs" , "01", "00", 4, noop ), + OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01", 4, status ), + OBDCommand("FREEZE_DTC" , "Freeze DTC" , "01", "02", 2, noop ), + OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03", 2, fuel_status ), + OBDCommand("LOAD" , "Calculated Engine Load" , "01", "04", 1, percent ), + OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05", 1, temp ), + OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06", 1, percent_centered ), + OBDCommand("Long_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07", 1, percent_centered ), + OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08", 1, percent_centered ), + OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09", 1, percent_centered ), + OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A", 1, fuel_pressure ), + OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "01", "0B", 1, intake_pressure ), + OBDCommand("RPM" , "Engine RPM" , "01", "0C", 2, rpm ), + OBDCommand("SPEED" , "Vehicle Speed" , "01", "0D", 1, speed ), + OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E", 1, timing_advance ), + OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F", 1, temp ), + OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10", 2, maf ), + OBDCommand("THROTTLE" , "Throttle Position" , "01", "11", 1, percent ), + OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12", 1, air_status ), + OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13", 1, noop ), + OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1" , "01", "14", 2, sensor_voltage ), + OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2" , "01", "15", 2, sensor_voltage ), + OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3" , "01", "16", 2, sensor_voltage ), + OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4" , "01", "17", 2, sensor_voltage ), + OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1" , "01", "18", 2, sensor_voltage ), + OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2" , "01", "19", 2, sensor_voltage ), + OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3" , "01", "1A", 2, sensor_voltage ), + OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4" , "01", "1B", 2, sensor_voltage ), + OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "01", "1C", 1, obd_compliance ), + OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D", 1, noop ), + OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E", 1, noop ), + OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F", 2, seconds ), + #OBDCommand("RUN_TIME_MIL" , "Engine Run Time MIL" , "01", "4D", sec_to_min ), + + OBDCommand("FUEL_TYPE" , "Fuel Type" , "01", "51", 1, fuel_type ), ] # mode 2 is the same as mode 1, but returns values from when the DTC occured diff --git a/obd/decoders.py b/obd/decoders.py index dfc26a11..e7e081ff 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -1,4 +1,5 @@ +import math from utils import Value, Unit, Test, unhex, unbin, bitstring, bitToBool from codes import * @@ -134,15 +135,43 @@ def status(_hex): return output -def mil(_hex): - v = bitstring(_hex) - return v[0] == '1' -def dtc_count(_hex): - v = bitstring(_hex) - return unbin(v[1:8]) +def fuel_status(_hex): + v = unhex(_hex) + i = int(math.sqrt(v)) # only a single bit should be on + + if i < len(FUEL_STATUS): + return FUEL_STATUS[i] + else: + return "Error: Unknown fuel status response" + + +def air_status(_hex): + v = unhex(_hex) + i = int(math.sqrt(v)) # only a single bit should be on + + if i < len(AIR_STATUS): + return AIR_STATUS[i] + else: + return "Error: Unknown air status response" + +def obd_compliance(_hex): + i = unhex(_hex) + + if i < len(OBD_COMPLIANCE): + return OBD_COMPLIANCE[i] + else: + return "Error: Unknown OBD compliance response" +def fuel_type(_hex): + i = unhex(_hex) + + if i < len(FUEL_TYPES): + return FUEL_TYPES[i] + else: + return "Error: Unknown fuel type response" + # Get the description of a DTC def describeCode(code): code.upper() From 53ee863bc32137d6cc24d6392fe481223eb059ef Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 18 Oct 2014 01:43:16 -0400 Subject: [PATCH 036/569] added the next 32 commands --- obd/commands.py | 105 ++++++++++++++++++++++++++++++++---------------- obd/decoders.py | 51 +++++++++++++++++++++-- obd/utils.py | 4 ++ 3 files changed, 122 insertions(+), 38 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 04535ad5..8d57ccce 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -66,41 +66,76 @@ def compute(result): __mode1__ = [ # mode 1 # sensor name description mode cmd bytes decoder - OBDCommand("PIDS" , "Supported PIDs" , "01", "00", 4, noop ), - OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01", 4, status ), - OBDCommand("FREEZE_DTC" , "Freeze DTC" , "01", "02", 2, noop ), - OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03", 2, fuel_status ), - OBDCommand("LOAD" , "Calculated Engine Load" , "01", "04", 1, percent ), - OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05", 1, temp ), - OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06", 1, percent_centered ), - OBDCommand("Long_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07", 1, percent_centered ), - OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08", 1, percent_centered ), - OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09", 1, percent_centered ), - OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A", 1, fuel_pressure ), - OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "01", "0B", 1, intake_pressure ), - OBDCommand("RPM" , "Engine RPM" , "01", "0C", 2, rpm ), - OBDCommand("SPEED" , "Vehicle Speed" , "01", "0D", 1, speed ), - OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E", 1, timing_advance ), - OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F", 1, temp ), - OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10", 2, maf ), - OBDCommand("THROTTLE" , "Throttle Position" , "01", "11", 1, percent ), - OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12", 1, air_status ), - OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13", 1, noop ), - OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1" , "01", "14", 2, sensor_voltage ), - OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2" , "01", "15", 2, sensor_voltage ), - OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3" , "01", "16", 2, sensor_voltage ), - OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4" , "01", "17", 2, sensor_voltage ), - OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1" , "01", "18", 2, sensor_voltage ), - OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2" , "01", "19", 2, sensor_voltage ), - OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3" , "01", "1A", 2, sensor_voltage ), - OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4" , "01", "1B", 2, sensor_voltage ), - OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "01", "1C", 1, obd_compliance ), - OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D", 1, noop ), - OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E", 1, noop ), - OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F", 2, seconds ), - #OBDCommand("RUN_TIME_MIL" , "Engine Run Time MIL" , "01", "4D", sec_to_min ), - - OBDCommand("FUEL_TYPE" , "Fuel Type" , "01", "51", 1, fuel_type ), + OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "01", "00", 4, noop ), + OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01", 4, status ), + OBDCommand("FREEZE_DTC" , "Freeze DTC" , "01", "02", 2, noop ), + OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03", 2, fuel_status ), + OBDCommand("LOAD" , "Calculated Engine Load" , "01", "04", 1, percent ), + OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05", 1, temp ), + OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06", 1, percent_centered ), + OBDCommand("Long_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07", 1, percent_centered ), + OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08", 1, percent_centered ), + OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09", 1, percent_centered ), + OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A", 1, fuel_pressure ), + OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "01", "0B", 1, pressure ), + OBDCommand("RPM" , "Engine RPM" , "01", "0C", 2, rpm ), + OBDCommand("SPEED" , "Vehicle Speed" , "01", "0D", 1, speed ), + OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E", 1, timing_advance ), + OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F", 1, temp ), + OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10", 2, maf ), + OBDCommand("THROTTLE" , "Throttle Position" , "01", "11", 1, percent ), + OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12", 1, air_status ), + OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13", 1, noop ), + OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1" , "01", "14", 2, sensor_voltage ), + OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2" , "01", "15", 2, sensor_voltage ), + OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3" , "01", "16", 2, sensor_voltage ), + OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4" , "01", "17", 2, sensor_voltage ), + OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1" , "01", "18", 2, sensor_voltage ), + OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2" , "01", "19", 2, sensor_voltage ), + OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3" , "01", "1A", 2, sensor_voltage ), + OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4" , "01", "1B", 2, sensor_voltage ), + OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "01", "1C", 1, obd_compliance ), + OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D", 1, noop ), + OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E", 1, noop ), + OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F", 2, seconds ), + + OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "01", "20", 4, noop ), + OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , "01", "21", 2, distance ), + OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , "01", "22", 2, fuel_pres_vac ), + OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , "01", "23", 2, fuel_pres_direct ), + OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , "01", "24", 4, sensor_voltage_big ), + OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , "01", "25", 4, sensor_voltage_big ), + OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , "01", "26", 4, sensor_voltage_big ), + OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , "01", "27", 4, sensor_voltage_big ), + OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , "01", "28", 4, sensor_voltage_big ), + OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , "01", "29", 4, sensor_voltage_big ), + OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , "01", "2A", 4, sensor_voltage_big ), + OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , "01", "2B", 4, sensor_voltage_big ), + OBDCommand("COMMANDED_EGR" , "Commanded EGR" , "01", "2C", 1, percent ), + OBDCommand("EGR_ERROR" , "EGR Error" , "01", "2D", 1, percent_centered ), + OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , "01", "2E", 1, percent ), + OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , "01", "2F", 1, percent ), + OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , "01", "30", 1, count ), + OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , "01", "31", 2, distance ), + OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , "01", "32", 2, evap_pressure ), + OBDCommand("BAROMETRIC_PRESSURE" , "Baromtric Pressure" , "01", "33", 1, pressure ), + OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , "01", "34", 4, current ), + OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , "01", "35", 4, current ), + OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , "01", "36", 4, current ), + OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , "01", "37", 4, current ), + OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , "01", "38", 4, current ), + OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , "01", "39", 4, current ), + OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , "01", "3A", 4, current ), + OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , "01", "3B", 4, current ), + OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , "01", "3C", 2, catalyst_temp ), + OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , "01", "3D", 2, catalyst_temp ), + OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , "01", "3E", 2, catalyst_temp ), + OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , "01", "3F", 2, catalyst_temp ), + + + #OBDCommand("RUN_TIME_MIL" , "Engine Run Time MIL" , "01", "4D", sec_to_min ), + + OBDCommand("FUEL_TYPE" , "Fuel Type" , "01", "51", 1, fuel_type ), ] # mode 2 is the same as mode 1, but returns values from when the DTC occured diff --git a/obd/decoders.py b/obd/decoders.py index e7e081ff..c6c6e3ad 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -16,6 +16,10 @@ def noop(_hex): Return Value object with value and units ''' +def count(_hex): + v = unhex(_hex) + return Value(v, Unit.COUNT) + # 0 to 100 % def percent(_hex): v = unhex(_hex) @@ -34,9 +38,28 @@ def temp(_hex): v = v - 40 return Value(v, Unit.C) +# -40 to 6513.5 C +def catalyst_temp(_hex): + v = unhex(_hex) + v = (v / 10.0) - 40 + return Value(v, Unit.C) + +# -128 to 128 mA +def current_centered(_hex): + v = unhex(_hex[4:8]) + v = (v / 256.0) - 128 + return Value(v, Unit.MA) + # 0 to 1.275 volts def sensor_voltage(_hex): v = unhex(_hex[0:2]) + v = v / 200.0 + return Value(v, Unit.VOLT) + +# 0 to 8 volts +def sensor_voltage_big(_hex): + v = unhex(_hex[4:8]) + v = (v * 8.0) / 65535 return Value(v, Unit.VOLT) # 0 to 765 kPa @@ -46,10 +69,29 @@ def fuel_pressure(_hex): return Value(v, Unit.KPA) # 0 to 255 kPa -def intake_pressure(_hex): +def pressure(_hex): v = unhex(_hex) return Value(v, Unit.KPA) +# 0 to 5177 kPa +def fuel_pres_vac(_hex): + v = unhex(_hex) + v = v * 0.079 + return Value(v, Unit.KPA) + +# 0 to 655,350 kPa +def fuel_pres_direct(_hex): + v = unhex(_hex) + v = v * 10 + return Value(v, Unit.KPA) + +# -8192 to 8192 Pa +# todo twos complement signed +def evap_pressure(_hex): + v = unhex(_hex) + v = v / 4.0 + return Value(v, Unit.PA) + # 0 to 16,383.75 RPM def rpm(_hex): v = unhex(_hex) @@ -61,7 +103,7 @@ def speed(_hex): v = unhex(_hex) return Value(v, Unit.KPH) -# -64 to 63.5 +# -64 to 63.5 degrees def timing_advance(_hex): v = unhex(_hex) v = (v - 128) / 2.0 @@ -78,7 +120,10 @@ def seconds(_hex): v = unhex(_hex) return Value(v, Unit.SECONDS) - +# 0 to 65535 km +def distance(_hex): + v = unhex(_hex) + return Value(v, Unit.KM) diff --git a/obd/utils.py b/obd/utils.py index 7a03bc43..e5e9f7e4 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -5,18 +5,22 @@ class Unit: NONE = None + COUNT = "Count" PERCENT = "Percent" VOLT = "Volt" F = "F" C = "C" SEC = "Second" MIN = "Minute" + PA = "Pa" KPA = "kPa" PSI = "PSI" KPH = "KPH" MPH = "MPH" DEGREES = "Degrees" GRAM_P_SEC = "Grams per Second" + MA = "mA" + KM = "km" class Value(): From 8ccd1fa31803ad18b8c689e35642a32bf34c703c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 18 Oct 2014 14:12:08 -0400 Subject: [PATCH 037/569] added twos complement decoding --- obd/decoders.py | 9 +++++---- obd/utils.py | 5 +++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index c6c6e3ad..2c228627 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -1,6 +1,6 @@ import math -from utils import Value, Unit, Test, unhex, unbin, bitstring, bitToBool +from utils import * from codes import * @@ -86,10 +86,11 @@ def fuel_pres_direct(_hex): return Value(v, Unit.KPA) # -8192 to 8192 Pa -# todo twos complement signed def evap_pressure(_hex): - v = unhex(_hex) - v = v / 4.0 + # decode the twos complement + a = twos_comp(unhex(_hex[0:2], 8)) + b = twos_comp(unhex(_hex[2:4], 8)) + v = ((a * 256.0) + b) / 4.0 return Value(v, Unit.PA) # 0 to 16,383.75 RPM diff --git a/obd/utils.py b/obd/utils.py index e5e9f7e4..45e13f85 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -57,6 +57,11 @@ def bitstring(_hex): def bitToBool(_bit): return (_bit == '1') +def twos_comp(val, num_bits): + """compute the 2's compliment of int value val""" + if( (val&(1<<(num_bits-1))) != 0 ): + val = val - (1< Date: Sat, 18 Oct 2014 14:19:00 -0400 Subject: [PATCH 038/569] updated special decoders to return value objects --- obd/decoders.py | 48 ++++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 2c228627..0fdae770 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -6,7 +6,7 @@ def noop(_hex): - return _hex + return Value(_hex, Unit.NONE) @@ -186,45 +186,56 @@ def fuel_status(_hex): v = unhex(_hex) i = int(math.sqrt(v)) # only a single bit should be on + v = "Error: Unknown fuel status response" + if i < len(FUEL_STATUS): - return FUEL_STATUS[i] - else: - return "Error: Unknown fuel status response" + v = FUEL_STATUS[i] + + return Value(v, Unit.NONE) def air_status(_hex): v = unhex(_hex) i = int(math.sqrt(v)) # only a single bit should be on + v = "Error: Unknown air status response" + if i < len(AIR_STATUS): - return AIR_STATUS[i] - else: - return "Error: Unknown air status response" + v = AIR_STATUS[i] + + return Value(v, Unit.NONE) def obd_compliance(_hex): i = unhex(_hex) + v = "Error: Unknown OBD compliance response" + if i < len(OBD_COMPLIANCE): - return OBD_COMPLIANCE[i] - else: - return "Error: Unknown OBD compliance response" + v = OBD_COMPLIANCE[i] + + return Value(v, Unit.NONE) def fuel_type(_hex): i = unhex(_hex) + v = "Error: Unknown fuel type response" + if i < len(FUEL_TYPES): - return FUEL_TYPES[i] - else: - return "Error: Unknown fuel type response" + v = FUEL_TYPES[i] + + return Value(v, Unit.NONE) # Get the description of a DTC def describeCode(code): code.upper() + + v = "Unknown or manufacturer specific code. Consult the internet." + if DTC.has_key(code): - return DTC[code] - else: - return "Unknown or manufacturer specific code. Consult the internet." + v = DTC[code] + + return Value(v, Unit.NONE) # converts 2 bytes of hex into a DTC code def dtc(_hex): @@ -234,7 +245,8 @@ def dtc(_hex): dtc += ['P', 'C', 'B', 'U'][unbin(bits[0:2]))] dtc += str(unbin(bits[2:4])) dtc += _hex[1:4] - return dtc + + return Value(dtc, Unit.NONE) # converts a frame of 2-byte DTCs into a list of DTCs def dtc_frame(_hex): @@ -248,4 +260,4 @@ def dtc_frame(_hex): codes.append(dtc(_hex[start:end])) - return codes + return Value(codes, Unit.NONE) From b74dc70bf75c542334f2324767a22d70ea7f6f0f Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 18 Oct 2014 15:26:14 -0400 Subject: [PATCH 039/569] added next page of OBD commands --- obd/commands.py | 70 ++++++++++++++++++++++++++++++++++++++++--------- obd/decoders.py | 47 +++++++++++++++++++++++++++------ obd/utils.py | 8 +++--- 3 files changed, 102 insertions(+), 23 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 8d57ccce..f2d0f322 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -64,13 +64,12 @@ def compute(result): # note, the SENSOR NAME field will be used as the dict key for that sensor __mode1__ = [ - # mode 1 - # sensor name description mode cmd bytes decoder + # sensor name description mode cmd bytes decoder OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "01", "00", 4, noop ), OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01", 4, status ), OBDCommand("FREEZE_DTC" , "Freeze DTC" , "01", "02", 2, noop ), OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03", 2, fuel_status ), - OBDCommand("LOAD" , "Calculated Engine Load" , "01", "04", 1, percent ), + OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , "01", "04", 1, percent ), OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05", 1, temp ), OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06", 1, percent_centered ), OBDCommand("Long_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07", 1, percent_centered ), @@ -83,22 +82,23 @@ def compute(result): OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E", 1, timing_advance ), OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F", 1, temp ), OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10", 2, maf ), - OBDCommand("THROTTLE" , "Throttle Position" , "01", "11", 1, percent ), + OBDCommand("THROTTLE_POS" , "Throttle Position" , "01", "11", 1, percent ), OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12", 1, air_status ), OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13", 1, noop ), - OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1" , "01", "14", 2, sensor_voltage ), - OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2" , "01", "15", 2, sensor_voltage ), - OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3" , "01", "16", 2, sensor_voltage ), - OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4" , "01", "17", 2, sensor_voltage ), - OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1" , "01", "18", 2, sensor_voltage ), - OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2" , "01", "19", 2, sensor_voltage ), - OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3" , "01", "1A", 2, sensor_voltage ), - OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4" , "01", "1B", 2, sensor_voltage ), + OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , "01", "14", 2, sensor_voltage ), + OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , "01", "15", 2, sensor_voltage ), + OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , "01", "16", 2, sensor_voltage ), + OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , "01", "17", 2, sensor_voltage ), + OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , "01", "18", 2, sensor_voltage ), + OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , "01", "19", 2, sensor_voltage ), + OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , "01", "1A", 2, sensor_voltage ), + OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , "01", "1B", 2, sensor_voltage ), OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "01", "1C", 1, obd_compliance ), OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D", 1, noop ), OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E", 1, noop ), OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F", 2, seconds ), + # sensor name description mode cmd bytes decoder OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "01", "20", 4, noop ), OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , "01", "21", 2, distance ), OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , "01", "22", 2, fuel_pres_vac ), @@ -132,6 +132,52 @@ def compute(result): OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , "01", "3E", 2, catalyst_temp ), OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , "01", "3F", 2, catalyst_temp ), + # sensor name description mode cmd bytes decoder + OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "01", "40", 4, noop ), + OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "01", "41", 4, ), + OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "01", "42", 2, ), + OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "01", "43", 2, ), + OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "01", "44", 2, ), + OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , "01", "45", 1, percent ), + OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , "01", "46", 1, temp ), + OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , "01", "47", 1, percent ), + OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , "01", "48", 1, percent ), + OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , "01", "49", 1, percent ), + OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , "01", "4A", 1, percent ), + OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , "01", "4B", 1, percent ), + OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , "01", "4C", 1, percent ), + OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , "01", "4D", 2, minutes ), + OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , "01", "4E", 2, minutes ), + OBDCommand("MAX_VALUES" , "Various Max values" , "01", "4F", 4, noop ), + OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , "01", "50", 4, max_maf ), + OBDCommand("FUEL_TYPE" , "Fuel Type" , "01", "51", 1, fuel_type ), + OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , "01", "52", 1, percent ), + OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , "01", "53", 2, abs_evap_pressure ), + OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , "01", "54", 2, evap_pressure_alt ), + OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , "01", "55", 2, percent_centered ), # todo: decode seconds value for banks 3 and 4 + OBDCommand("Long_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , "01", "56", 2, percent_centered ), + OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , "01", "57", 2, percent_centered ), + OBDCommand("Long_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , "01", "58", 2, percent_centered ), + OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , "01", "59", 2, fuel_pres_direct ), + OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , "01", "5A", 1, percent ), + OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , "01", "5B", 1, percent ), + OBDCommand("OIL_TEMP" , "Engine oil temperature" , "01", "5C", 1, temp ), + OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , "01", "5D", 2, inject_timing ), + OBDCommand("FUEL_RATE" , "Engine fuel rate" , "01", "5E", 2, fuel_rate ), + OBDCommand("EMISSION_REQ" , "Designed emission requirements" , "01", "5F", 1, noop ), + + + + + + + + + + + + + #OBDCommand("RUN_TIME_MIL" , "Engine Run Time MIL" , "01", "4D", sec_to_min ), diff --git a/obd/decoders.py b/obd/decoders.py index 0fdae770..ea946f16 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -4,13 +4,10 @@ from codes import * - +# hex in, hex out def noop(_hex): return Value(_hex, Unit.NONE) - - - ''' Sensor decoders Return Value object with value and units @@ -22,13 +19,13 @@ def count(_hex): # 0 to 100 % def percent(_hex): - v = unhex(_hex) + v = unhex(_hex[0:2]) v = v * 100.0 / 255.0 return Value(v, Unit.PERCENT) # -100 to 100 % def percent_centered(_hex): - v = unhex(_hex) + v = unhex(_hex[0:2]) v = (v - 128) * 100.0 / 128.0 return Value(v, Unit.PERCENT) @@ -93,6 +90,18 @@ def evap_pressure(_hex): v = ((a * 256.0) + b) / 4.0 return Value(v, Unit.PA) +# 0 to 327.675 kPa +def abs_evap_pressure(_hex): + v = unhex(_hex) + v = v / 200 + return Value(v, Unit.KPA) + +# -32767 to 32768 Pa +def evap_pressure_alt(_hex): + v = unhex(_hex) + v = v - 32767 + return Value(v, Unit.PA) + # 0 to 16,383.75 RPM def rpm(_hex): v = unhex(_hex) @@ -110,22 +119,44 @@ def timing_advance(_hex): v = (v - 128) / 2.0 return Value(v, Unit.DEGREES) +# -210 to 301 degrees +def inject_timing(_hex): + v = unhex(_hex) + v = (v - 26880) / 128.0 + return Value(v, Unit.DEGREES) + # 0 to 655.35 grams/sec def maf(_hex): v = unhex(_hex) v = v / 100.0 - return Value(v, Unit.GRAM_P_SEC) + return Value(v, Unit.GPS) -# 0 to 655.35 seconds +# 0 to 2550 grams/sec +def max_maf(_hex): + v = unhex(_hex[0:2]) + v = v * 10 + return Value(v, Unit.GPS) + +# 0 to 65535 seconds def seconds(_hex): v = unhex(_hex) return Value(v, Unit.SECONDS) +# 0 to 65535 minutes +def minutes(_hex): + v = unhex(_hex) + return Value(v, Unit.MIN) + # 0 to 65535 km def distance(_hex): v = unhex(_hex) return Value(v, Unit.KM) +# 0 to 3212 Liters/hour +def fuel_rate(_hex): + v = unhex(_hex) + v = v * 0.05 + return Value(v, Unit.LPH) ''' diff --git a/obd/utils.py b/obd/utils.py index 45e13f85..de5c5637 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -5,6 +5,7 @@ class Unit: NONE = None + RATIO = "Ratio" COUNT = "Count" PERCENT = "Percent" VOLT = "Volt" @@ -15,12 +16,13 @@ class Unit: PA = "Pa" KPA = "kPa" PSI = "PSI" - KPH = "KPH" - MPH = "MPH" + KPH = "Kilometers per Hour" + MPH = "Miles per Hour" DEGREES = "Degrees" - GRAM_P_SEC = "Grams per Second" + GPS = "Grams per Second" MA = "mA" KM = "km" + LPH = "Liters per Hour" class Value(): From 1e5219d146ae76e0a8dfa7a3d555757b853fb752 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 18 Oct 2014 15:43:37 -0400 Subject: [PATCH 040/569] constrained number of bytes in response --- obd/commands.py | 48 ++++++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index f2d0f322..1d099b64 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -25,17 +25,19 @@ from decoders import * +from Utils import Value, Unit class OBDCommand(): - def __init__(self, sensorname, desc, mode, pid, pid, returnBytes, decoder): + def __init__(self, sensorname, desc, mode, pid, pid, returnBytes, decoder, supported=False): self.sensorname = sensorname self.desc = desc self.mode = mode self.pid = pid self.bytes = returnBytes # number of bytes expected in return self.decode = decoder + self.supported = supported def clone(self): return OBDCommand(self.sensorname, @@ -48,15 +50,25 @@ def clone(self): def getCommand(self): return self.mode + self.pid - def compute(result): - if "NODATA" in result: - return "" + def compute(self, _hex): + if "NODATA" in _hex: + return Value("No Data", Unit.NONE) else: - if (self.bytes > 0) and (len(result) != self.bytes * 2): - print "Receieved unexpected number of bytes, trying to parse anyways..." + + # constrain number of bytes in response + if (self.bytes > 0): # zero bytes means flexible response + + diff = (self.bytes * 2) - len(_hex) # length discrepency in number of hex digits + + if diff > 0: + print "Receieved less data than expected, trying to parse anyways..." + _hex += ('0' * diff) # pad the right side with zeros + elif diff < 0: + print "Receieved more data than expected, trying to parse anyways..." + _hex = _hex[:diff] # chop off the right side to fit # return the decoded value object - return self.decode(result) + return self.decode(_hex) @@ -65,7 +77,7 @@ def compute(result): __mode1__ = [ # sensor name description mode cmd bytes decoder - OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "01", "00", 4, noop ), + OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "01", "00", 4, noop , True), # the first PID getter is assumed to be supported OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01", 4, status ), OBDCommand("FREEZE_DTC" , "Freeze DTC" , "01", "02", 2, noop ), OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03", 2, fuel_status ), @@ -148,7 +160,7 @@ def compute(result): OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , "01", "4C", 1, percent ), OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , "01", "4D", 2, minutes ), OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , "01", "4E", 2, minutes ), - OBDCommand("MAX_VALUES" , "Various Max values" , "01", "4F", 4, noop ), + OBDCommand("MAX_VALUES" , "Various Max values" , "01", "4F", 4, noop ), # todo: decode this OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , "01", "50", 4, max_maf ), OBDCommand("FUEL_TYPE" , "Fuel Type" , "01", "51", 1, fuel_type ), OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , "01", "52", 1, percent ), @@ -165,25 +177,9 @@ def compute(result): OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , "01", "5D", 2, inject_timing ), OBDCommand("FUEL_RATE" , "Engine fuel rate" , "01", "5E", 2, fuel_rate ), OBDCommand("EMISSION_REQ" , "Designed emission requirements" , "01", "5F", 1, noop ), - - - - - - - - - - - - - - - #OBDCommand("RUN_TIME_MIL" , "Engine Run Time MIL" , "01", "4D", sec_to_min ), - - OBDCommand("FUEL_TYPE" , "Fuel Type" , "01", "51", 1, fuel_type ), ] + # mode 2 is the same as mode 1, but returns values from when the DTC occured __mode2__ = [] for c in __mode1__: From ae4d949f65b76cc888802a1209310ad2e36f6444 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 18 Oct 2014 16:02:33 -0400 Subject: [PATCH 041/569] created commands object with access by PID and name --- obd/__init__.py | 2 +- obd/commands.py | 67 +++++++++++++++++++++++++++---------------------- obd/obd.py | 2 +- obd/port.py | 26 ------------------- 4 files changed, 39 insertions(+), 58 deletions(-) diff --git a/obd/__init__.py b/obd/__init__.py index 055542ab..56d09095 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -1,4 +1,4 @@ from obd import OBD -from commands import sensors, specials, commands +from commands import commands from utils import scanSerial diff --git a/obd/commands.py b/obd/commands.py index 1d099b64..e6949dc1 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -72,6 +72,9 @@ def compute(self, _hex): +''' +Define command tables +''' # note, the SENSOR NAME field will be used as the dict key for that sensor @@ -197,7 +200,7 @@ def compute(self, _hex): __mode4__ = [ # sensor name description mode cmd bytes decoder - OBDCommand("CLEAR_DTC" , "Clear DTCs" , "04", "" , 0, noop ), + OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", "" , 0, noop ), ] __mode7__ [ @@ -207,32 +210,36 @@ def compute(self, _hex): - -# assemble the commands by mode -commands = [ - [], - __mode1__, - __mode2__, - __mode3__, - __mode4__, - [], - [], - __mode7__ -] - - -class sensors(): - pass - -class specials(): - pass - - -# allow sensor commands to be accessed by name -for m in commands: - for c in m: - # if the command has no decoder, it is considered a special - if c.decode == noop: - specials.__dict__[c.sensorname] = c - else: - sensors.__dict__[c.sensorname] = c +''' +Assemble the command tables by mode, and allow access by sensor name +''' + +class Commands(): + def __init__(self): + self.modes = [ + [], + __mode1__, + __mode2__, + __mode3__, + __mode4__, + [], + [], + __mode7__ + ] + + # allow commands to be accessed by sensor name + for m in self.modes: + for c in m: + self.__dict__[c.sensorname] = c + + def __getitem__(self, key): + return self.modes[key] + + def __len__(self): + l = 0 + for m in self.modes: + l += len(m) + return l + +# export this object +commands = Commands() diff --git a/obd/obd.py b/obd/obd.py index 44ffc983..60d19c95 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -3,7 +3,7 @@ import time from port import OBDPort, State -from commands import sensors, specials, commands +from commands import commands from obd_utils import scanSerial diff --git a/obd/port.py b/obd/port.py index 05b71110..1fb9adfa 100644 --- a/obd/port.py +++ b/obd/port.py @@ -197,26 +197,6 @@ def get_sensor_value(self, sensor): return data - ''' - def get_tests_MIL(self): - statusText=["Unsupported","Supported - Completed","Unsupported","Supported - Incompleted"] - - statusRes = self.sensor(1)[1] #GET values - statusTrans = [] #translate values to text - - statusTrans.append(str(statusRes[0])) #DTCs - - if statusRes[1]==0: #MIL - statusTrans.append("Off") - else: - statusTrans.append("On") - - for i in range(2,len(statusRes)): #Tests - statusTrans.append(statusText[statusRes[i]]) - - return statusTrans - ''' - # # fixme: j1979 specifies that the program should poll until the number # of returned DTCs matches the number indicated by a call to PID 01 @@ -268,9 +248,3 @@ def get_dtc(self): DTCCodes.append(["Passive",DTCStr]) return DTCCodes - - def clear_dtc(self): - """Clears all DTCs and freeze frame data""" - self.send_command(CLEAR_DTC_COMMAND) - r = self.get_result() - return r From 9f4bcb7538689d0d0e2de40cab8154c26b867a87 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 18 Oct 2014 16:14:51 -0400 Subject: [PATCH 042/569] misc bug fixes --- obd/commands.py | 36 ++++++++++++++++++------------------ obd/decoders.py | 6 +++++- obd/obd.py | 2 +- obd/port.py | 12 +++++------- 4 files changed, 29 insertions(+), 27 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index e6949dc1..1ee2cff1 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -25,13 +25,13 @@ from decoders import * -from Utils import Value, Unit +from utils import Value, Unit class OBDCommand(): - def __init__(self, sensorname, desc, mode, pid, pid, returnBytes, decoder, supported=False): - self.sensorname = sensorname + def __init__(self, name, desc, mode, pid, returnBytes, decoder, supported=False): + self.name = name self.desc = desc self.mode = mode self.pid = pid @@ -40,7 +40,7 @@ def __init__(self, sensorname, desc, mode, pid, pid, returnBytes, decoder, suppo self.supported = supported def clone(self): - return OBDCommand(self.sensorname, + return OBDCommand(self.name, self.desc, self.mode, self.pid, @@ -134,14 +134,14 @@ def compute(self, _hex): OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , "01", "31", 2, distance ), OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , "01", "32", 2, evap_pressure ), OBDCommand("BAROMETRIC_PRESSURE" , "Baromtric Pressure" , "01", "33", 1, pressure ), - OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , "01", "34", 4, current ), - OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , "01", "35", 4, current ), - OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , "01", "36", 4, current ), - OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , "01", "37", 4, current ), - OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , "01", "38", 4, current ), - OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , "01", "39", 4, current ), - OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , "01", "3A", 4, current ), - OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , "01", "3B", 4, current ), + OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , "01", "34", 4, current_centered ), + OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , "01", "35", 4, current_centered ), + OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , "01", "36", 4, current_centered ), + OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , "01", "37", 4, current_centered ), + OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , "01", "38", 4, current_centered ), + OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , "01", "39", 4, current_centered ), + OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , "01", "3A", 4, current_centered ), + OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , "01", "3B", 4, current_centered ), OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , "01", "3C", 2, catalyst_temp ), OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , "01", "3D", 2, catalyst_temp ), OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , "01", "3E", 2, catalyst_temp ), @@ -149,10 +149,10 @@ def compute(self, _hex): # sensor name description mode cmd bytes decoder OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "01", "40", 4, noop ), - OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "01", "41", 4, ), - OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "01", "42", 2, ), - OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "01", "43", 2, ), - OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "01", "44", 2, ), + OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "01", "41", 4, todo ), + OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "01", "42", 2, todo ), + OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "01", "43", 2, todo ), + OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "01", "44", 2, todo ), OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , "01", "45", 1, percent ), OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , "01", "46", 1, temp ), OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , "01", "47", 1, percent ), @@ -203,7 +203,7 @@ def compute(self, _hex): OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", "" , 0, noop ), ] -__mode7__ [ +__mode7__ = [ # sensor name description mode cmd bytes decoder OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", "" , 0, noop ), ] @@ -230,7 +230,7 @@ def __init__(self): # allow commands to be accessed by sensor name for m in self.modes: for c in m: - self.__dict__[c.sensorname] = c + self.__dict__[c.name] = c def __getitem__(self, key): return self.modes[key] diff --git a/obd/decoders.py b/obd/decoders.py index ea946f16..d62bd3fc 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -4,6 +4,10 @@ from codes import * +# todo +def todo(_hex): + return Value(_hex, Unit.NONE) + # hex in, hex out def noop(_hex): return Value(_hex, Unit.NONE) @@ -273,7 +277,7 @@ def dtc(_hex): dtc = "" bits = bitstring(_hex[0]) - dtc += ['P', 'C', 'B', 'U'][unbin(bits[0:2]))] + dtc += ['P', 'C', 'B', 'U'][unbin(bits[0:2])] dtc += str(unbin(bits[2:4])) dtc += _hex[1:4] diff --git a/obd/obd.py b/obd/obd.py index 60d19c95..f2ee42a2 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -4,7 +4,7 @@ from port import OBDPort, State from commands import commands -from obd_utils import scanSerial +from utils import scanSerial diff --git a/obd/port.py b/obd/port.py index 1fb9adfa..c0e8ddd0 100644 --- a/obd/port.py +++ b/obd/port.py @@ -25,9 +25,7 @@ import serial import string import time - -import obd_sensors -from obd_utils import hex_to_int +from utils import unhex GET_DTC_COMMAND = "03" @@ -218,8 +216,8 @@ def get_dtc(self): res = self.get_result() print "DTC result:" + res for i in range(0, 3): - val1 = hex_to_int(res[3+i*6:5+i*6]) - val2 = hex_to_int(res[6+i*6:8+i*6]) #get DTC codes from response (3 DTC each 2 bytes) + val1 = unhex(res[3+i*6:5+i*6]) + val2 = unhex(res[6+i*6:8+i*6]) #get DTC codes from response (3 DTC each 2 bytes) val = (val1<<8)+val2 #DTC val as int if val==0: #skip fill of last packet @@ -237,8 +235,8 @@ def get_dtc(self): print "DTC freeze result:" + res for i in range(0, 3): - val1 = hex_to_int(res[3+i*6:5+i*6]) - val2 = hex_to_int(res[6+i*6:8+i*6]) #get DTC codes from response (3 DTC each 2 bytes) + val1 = unhex(res[3+i*6:5+i*6]) + val2 = unhex(res[6+i*6:8+i*6]) #get DTC codes from response (3 DTC each 2 bytes) val = (val1<<8)+val2 #DTC val as int if val==0: #skip fill of last packet From 02510a6e8a794b32c2c633e7ef38b21dbd1b49b2 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 18 Oct 2014 16:23:03 -0400 Subject: [PATCH 043/569] adapt sensor reader in port for updated arch --- obd/obd.py | 18 ++++++++---------- obd/port.py | 7 ++++--- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/obd/obd.py b/obd/obd.py index f2ee42a2..fcf80e22 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -17,7 +17,7 @@ def __init__(self, portstr=None): # initialize by connecting and loading sensors if self.connect(portstr): - self.load_sensors() + self.load_commands() def connect(self, portstr=None): @@ -47,7 +47,7 @@ def get_port_name(self): return self.port.get_port_name() - def load_sensors(self): + def load_commands(self): """ queries for available sensors, and compiles lists of indices and sensor objects """ self.supportedCommands = [] @@ -55,7 +55,7 @@ def load_sensors(self): # Find supported sensors - by getting PIDs from OBD (sensor zero) # its a string of binary 01010101010101 # 1 means the sensor is supported - supported = self.sendCommand(commands[1][0]) # mode 01, command 00 + supported = self.send_command(commands[1][0]) # mode 01, command 00 count = min(len(supported), len(commands[1])) @@ -67,15 +67,15 @@ def load_sensors(self): self.supportedCommands.append(c) - def printCommands(self): + def print_commands(self): for c in self.supportedCommands: print str(c) - def hasCommand(self, c): + def has_command(self, c): return c.supported - def sendCommand(self, command): - return self.port.get_sensor_value(sensor) + def send_command(self, command): + return self.port.get_sensor_value(command) @@ -83,11 +83,9 @@ def sendCommand(self, command): if __name__ == "__main__": o = OBD() - #o.connect() time.sleep(3) if not o.is_connected(): print "Not connected" else: print "Connected to " + o.get_port_name() - #o.load_sensors() - o.printSensors() + o.print_commands() diff --git a/obd/port.py b/obd/port.py index c0e8ddd0..403c3d64 100644 --- a/obd/port.py +++ b/obd/port.py @@ -180,16 +180,17 @@ def get_result(self): return "NORESPONSE" # get sensor value from command - def get_sensor_value(self, sensor): + def get_sensor_value(self, command): - cmd = sensor.cmd + cmd = command.getCommand() self.send_command(cmd) data = self.get_result() if data: + print "RX: ", data data = self.interpret_result(data) if data != "NODATA": - data = sensor.value(data) + data = command.compute(data) else: return "NORESPONSE" From d002abcaff9a097a7053f9b0906ca24ef9461a86 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 18 Oct 2014 16:57:30 -0400 Subject: [PATCH 044/569] type checking and update readme --- README.md | 37 +++++++++++++++++++++++-------------- obd/commands.py | 16 ++++++++++++---- obd/decoders.py | 5 +++++ obd/obd.py | 9 ++++++--- 4 files changed, 46 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 9c15484a..484b4b15 100644 --- a/README.md +++ b/README.md @@ -56,15 +56,10 @@ Sensors can also be explicitly targetted for values. The hasSensor() function wi print connection.valueOf(obd.sensors.RPM) # get value of sensor -Here are the currently supported sensors with pyOBD-IO: +Here are a few of the currently supported commands (for a full list, see commands.py): -+ S-S DTC Cleared + Calculated Engine Load + Engine Coolant Temperature -+ Short Term Fuel Trim - Bank 1 -+ Long Term Fuel Trim - Bank 1 -+ Short Term Fuel Trim - Bank 2 -+ Long Term Fuel Trim - Bank 2 + Fuel Pressure + Intake Manifold Pressure + Engine RPM @@ -73,15 +68,29 @@ Here are the currently supported sensors with pyOBD-IO: + Intake Air Temp + Air Flow Rate (MAF) + Throttle Position -+ O2: Bank 1 - Sensor 1 -+ O2: Bank 1 - Sensor 2 -+ O2: Bank 1 - Sensor 3 -+ O2: Bank 1 - Sensor 4 -+ O2: Bank 2 - Sensor 1 -+ O2: Bank 2 - Sensor 2 -+ O2: Bank 2 - Sensor 3 -+ O2: Bank 2 - Sensor 4 + Engine Run Time ++ Distance Traveled with MIL on ++ Fuel Rail Pressure (relative to vacuum) ++ Fuel Rail Pressure (direct inject) ++ Fuel Level Input ++ Number of warm-ups since codes cleared ++ Distance traveled since codes cleared ++ Evaporative system vapor pressure ++ Baromtric Pressure ++ Control module voltage ++ Relative throttle position ++ Ambient air temperature ++ Commanded throttle actuator ++ Time run with MIL on ++ Time since trouble codes cleared ++ Fuel Type ++ Ethanol Fuel Percent ++ Fuel rail pressure (absolute) ++ Relative accelerator pedal position ++ Hybrid battery pack remaining life ++ Engine oil temperature ++ Fuel injection timing ++ Engine fuel rate Enjoy and drive safe! diff --git a/obd/commands.py b/obd/commands.py index 1ee2cff1..fd744658 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -70,6 +70,9 @@ def compute(self, _hex): # return the decoded value object return self.decode(_hex) + def __str__(self): + return self.desc + ''' @@ -80,7 +83,7 @@ def compute(self, _hex): __mode1__ = [ # sensor name description mode cmd bytes decoder - OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "01", "00", 4, noop , True), # the first PID getter is assumed to be supported + OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "01", "00", 4, pid , True), # the first PID getter is assumed to be supported OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01", 4, status ), OBDCommand("FREEZE_DTC" , "Freeze DTC" , "01", "02", 2, noop ), OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03", 2, fuel_status ), @@ -114,7 +117,7 @@ def compute(self, _hex): OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F", 2, seconds ), # sensor name description mode cmd bytes decoder - OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "01", "20", 4, noop ), + OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "01", "20", 4, pid ), OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , "01", "21", 2, distance ), OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , "01", "22", 2, fuel_pres_vac ), OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , "01", "23", 2, fuel_pres_direct ), @@ -148,7 +151,7 @@ def compute(self, _hex): OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , "01", "3F", 2, catalyst_temp ), # sensor name description mode cmd bytes decoder - OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "01", "40", 4, noop ), + OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "01", "40", 4, pid ), OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "01", "41", 4, todo ), OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "01", "42", 2, todo ), OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "01", "43", 2, todo ), @@ -216,6 +219,8 @@ def compute(self, _hex): class Commands(): def __init__(self): + + # allow commands to be accessed by mode and PID self.modes = [ [], __mode1__, @@ -233,7 +238,10 @@ def __init__(self): self.__dict__[c.name] = c def __getitem__(self, key): - return self.modes[key] + if isinstance(key, int): + return self.modes[key] + elif isinstance(key, basestring): + return self.__dict__[key] def __len__(self): l = 0 diff --git a/obd/decoders.py b/obd/decoders.py index d62bd3fc..4ba8b79b 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -12,6 +12,11 @@ def todo(_hex): def noop(_hex): return Value(_hex, Unit.NONE) +# hex in, bitstring out +def pid(_hex): + v = bitstring(_hex) + return Value(v, Unit.NONE) + ''' Sensor decoders Return Value object with value and units diff --git a/obd/obd.py b/obd/obd.py index fcf80e22..31389c08 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -48,7 +48,7 @@ def get_port_name(self): def load_commands(self): - """ queries for available sensors, and compiles lists of indices and sensor objects """ + """ queries for available PIDs, and compiles lists of command objects """ self.supportedCommands = [] @@ -74,8 +74,11 @@ def print_commands(self): def has_command(self, c): return c.supported - def send_command(self, command): - return self.port.get_sensor_value(command) + def query(self, command): + if self.has_command(command): + return self.port.get_sensor_value(command) + else: + From 5d165f46d14ed03cd5e25cab2d710b572f8d62ce Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 18 Oct 2014 17:31:38 -0400 Subject: [PATCH 045/569] updated sensor loader --- obd/commands.py | 29 +++++++++++++++++++++++++++-- obd/obd.py | 29 +++++++++++++++++------------ 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index fd744658..fbcffe3d 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -45,11 +45,18 @@ def clone(self): self.mode, self.pid, self.bytes, - self.decode) + self.decode, + self.supported) def getCommand(self): return self.mode + self.pid + def getModeInt(self): + return unhex(self.mode) + + def getPidInt(self): + return unhex(self.pid) + def compute(self, _hex): if "NODATA" in _hex: return Value("No Data", Unit.NONE) @@ -79,7 +86,8 @@ def __str__(self): Define command tables ''' -# note, the SENSOR NAME field will be used as the dict key for that sensor +# NOTE: the SENSOR NAME field will be used as the dict key for that sensor +# NOTE: commands MUST be in PID order, one command per PID (for fast lookup using __mode1__[pid]) __mode1__ = [ # sensor name description mode cmd bytes decoder @@ -249,5 +257,22 @@ def __len__(self): l += len(m) return l + # returns a list of PID GET commands + def pid_getters(self): + getters = [] + for m in self.modes: + for c in m: + if c.decode == pid: # GET commands have a special decoder + getter.append(c) + return getters + + # sets the boolean for + def set_supported(self, mode, pid, v): + if isinstance(v, bool): + if (mode < len(self.modes)) and (pid < len(self.modes[mode])): + self.modes[mode][pid].supported = v + else: + print "set_supported only accepts boolean values" + # export this object commands = Commands() diff --git a/obd/obd.py b/obd/obd.py index 31389c08..6d932947 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -48,23 +48,28 @@ def get_port_name(self): def load_commands(self): - """ queries for available PIDs, and compiles lists of command objects """ + """ queries for available PIDs, sets their support status, and compiles a list of command objects """ self.supportedCommands = [] - # Find supported sensors - by getting PIDs from OBD (sensor zero) - # its a string of binary 01010101010101 - # 1 means the sensor is supported - supported = self.send_command(commands[1][0]) # mode 01, command 00 + pid_getters = commands.pid_getters() - count = min(len(supported), len(commands[1])) + for get in pid_getters: + # GET commands should sequentialy turn themselves on (become marked as supported) + # MODE 1 PID 0 is marked supported by default + if self.has_command(get): + supported = self.query(get).value # string of binary 01010101010101 - # loop through PIDs binary - for i in range(count): - if supported[i] == "1": - c = commands[1][i] - c.supported = True - self.supportedCommands.append(c) + # loop through PIDs binary + for i in range(len(supported)): + if supported[i] == "1": + + mode = get.getModeInt() + pid = get.getPidInt() + i + 1 + + c = commands[mode][pid] + c.supported = True + self.supportedCommands.append(c) def print_commands(self): From 4cb84d9b2ee6c4f4f73a1baabc9cdbb02ca2f7a9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 18 Oct 2014 17:36:08 -0400 Subject: [PATCH 046/569] updated name in readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 484b4b15..2e930042 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -pyOBD-IO +python-OBD ======== ##### A python module for handling realtime sensor data from OBD-II vehicle ports @@ -17,7 +17,7 @@ This library is forked from: ### Usage -After installing the library, simply import pyobd, and create a new OBD connection object. By default, pyOBD-IO will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the scanSerial helper retrieve a list of connected ports +After installing the library, simply import pyobd, and create a new OBD connection object. By default, python-OBD will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the scanSerial helper retrieve a list of connected ports import obd @@ -34,7 +34,7 @@ After installing the library, simply import pyobd, and create a new OBD connecti connection = obd.OBD(ports[0]) # connect to the first port in the list -Once a connection is made, pyOBD-IO will load a list of the available sensors in your car. A "Sensor" in pyOBD-IO is an object containing its name, units, and retrieval functions. To get the value of a sensor, call the valueOf() function with a sensor object as an argument. +Once a connection is made, python-OBD will load a list of the available sensors in your car. A "Sensor" in python-OBD is an object containing its name, units, and retrieval functions. To get the value of a sensor, call the valueOf() function with a sensor object as an argument. import obd From 9d70933307c7a54d7f37c92febbada64803112e7 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 18 Oct 2014 19:15:12 -0400 Subject: [PATCH 047/569] updated readme, modified response object to contain raw hex --- README.md | 16 ++--- obd/commands.py | 15 +++-- obd/decoders.py | 175 +++++++++++++++++++++++++++++------------------- obd/utils.py | 11 ++- 4 files changed, 130 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 2e930042..b4772d7a 100644 --- a/README.md +++ b/README.md @@ -34,26 +34,26 @@ After installing the library, simply import pyobd, and create a new OBD connecti connection = obd.OBD(ports[0]) # connect to the first port in the list -Once a connection is made, python-OBD will load a list of the available sensors in your car. A "Sensor" in python-OBD is an object containing its name, units, and retrieval functions. To get the value of a sensor, call the valueOf() function with a sensor object as an argument. +Once a connection is made, python-OBD will load a list of the available commands in your car. A "Command" in python-OBD is an object used to query specific information from the vehicle. A command object contains its name, units, codes, and decoding functions. To get the value of a sensor, call the query() function with that sensor's command as an argument. import obd connection = obd.OBD() - for sensor in connection.supportedSensors: - print str(sensor) # prints the sensor name - print connection.valueOf(sensor) # gets and prints the sensor's value - print sensor.unit # prints the sensors units + for command in connection.supportedCommands: + print str(command) # prints the command name + response = connection.query(command) # sends the command, and returns the decoded response + print response.value, response.unit # prints the data and units returned from the car -Sensors can also be explicitly targetted for values. The hasSensor() function will determine whether or not your car has the requested sensor. +Commands can also be accessed explicitly, either by name, or by code value. The has_command() function will determine whether or not your car supports the requested command. import obd connection = obd.OBD() - if connection.hasSensor(obd.sensors.RPM): # check for existance of sensor - print connection.valueOf(obd.sensors.RPM) # get value of sensor + if connection.has_command(obd.commands.RPM): # check for existance of sensor + print connection.query(obd.commands.RPM) # get and print value of sensor Here are a few of the currently supported commands (for a full list, see commands.py): diff --git a/obd/commands.py b/obd/commands.py index fbcffe3d..754dfb3a 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -25,7 +25,7 @@ from decoders import * -from utils import Value, Unit +from utils import Response, Unit @@ -58,9 +58,10 @@ def getPidInt(self): return unhex(self.pid) def compute(self, _hex): - if "NODATA" in _hex: - return Value("No Data", Unit.NONE) - else: + # create the response object with the raw hex recieved + r = Response(_hex) + + if "NODATA" not in _hex: # constrain number of bytes in response if (self.bytes > 0): # zero bytes means flexible response @@ -74,8 +75,10 @@ def compute(self, _hex): print "Receieved more data than expected, trying to parse anyways..." _hex = _hex[:diff] # chop off the right side to fit - # return the decoded value object - return self.decode(_hex) + # decoded value + self.decode(r) + + return r def __str__(self): return self.desc diff --git a/obd/decoders.py b/obd/decoders.py index 4ba8b79b..02e7bf5a 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -5,167 +5,195 @@ # todo -def todo(_hex): - return Value(_hex, Unit.NONE) +def todo(r): + _hex = r.raw_hex + r.set(_hex, Unit.NONE) # hex in, hex out -def noop(_hex): - return Value(_hex, Unit.NONE) +def noop(r): + _hex = r.raw_hex + r.set(_hex, Unit.NONE) # hex in, bitstring out -def pid(_hex): +def pid(r): + _hex = r.raw_hex v = bitstring(_hex) - return Value(v, Unit.NONE) + r.set(v, Unit.NONE) ''' Sensor decoders Return Value object with value and units ''' -def count(_hex): +def count(r): + _hex = r.raw_hex v = unhex(_hex) - return Value(v, Unit.COUNT) + r.set(v, Unit.COUNT) # 0 to 100 % -def percent(_hex): +def percent(r): + _hex = r.raw_hex v = unhex(_hex[0:2]) v = v * 100.0 / 255.0 - return Value(v, Unit.PERCENT) + r.set(v, Unit.PERCENT) # -100 to 100 % -def percent_centered(_hex): +def percent_centered(r): + _hex = r.raw_hex v = unhex(_hex[0:2]) v = (v - 128) * 100.0 / 128.0 - return Value(v, Unit.PERCENT) + r.set(v, Unit.PERCENT) # -40 to 215 C -def temp(_hex): +def temp(r): + _hex = r.raw_hex v = unhex(_hex) v = v - 40 - return Value(v, Unit.C) + r.set(v, Unit.C) # -40 to 6513.5 C -def catalyst_temp(_hex): +def catalyst_temp(r): + _hex = r.raw_hex v = unhex(_hex) v = (v / 10.0) - 40 - return Value(v, Unit.C) + r.set(v, Unit.C) # -128 to 128 mA -def current_centered(_hex): +def current_centered(r): + _hex = r.raw_hex v = unhex(_hex[4:8]) v = (v / 256.0) - 128 - return Value(v, Unit.MA) + r.set(v, Unit.MA) # 0 to 1.275 volts -def sensor_voltage(_hex): +def sensor_voltage(r): + _hex = r.raw_hex v = unhex(_hex[0:2]) v = v / 200.0 - return Value(v, Unit.VOLT) + r.set(v, Unit.VOLT) # 0 to 8 volts -def sensor_voltage_big(_hex): +def sensor_voltage_big(r): + _hex = r.raw_hex v = unhex(_hex[4:8]) v = (v * 8.0) / 65535 - return Value(v, Unit.VOLT) + r.set(v, Unit.VOLT) # 0 to 765 kPa -def fuel_pressure(_hex): +def fuel_pressure(r): + _hex = r.raw_hex v = unhex(_hex) v = v * 3 - return Value(v, Unit.KPA) + r.set(v, Unit.KPA) # 0 to 255 kPa -def pressure(_hex): +def pressure(r): + _hex = r.raw_hex v = unhex(_hex) - return Value(v, Unit.KPA) + r.set(v, Unit.KPA) # 0 to 5177 kPa -def fuel_pres_vac(_hex): +def fuel_pres_vac(r): + _hex = r.raw_hex v = unhex(_hex) v = v * 0.079 - return Value(v, Unit.KPA) + r.set(v, Unit.KPA) # 0 to 655,350 kPa -def fuel_pres_direct(_hex): +def fuel_pres_direct(r): + _hex = r.raw_hex v = unhex(_hex) v = v * 10 - return Value(v, Unit.KPA) + r.set(v, Unit.KPA) # -8192 to 8192 Pa -def evap_pressure(_hex): +def evap_pressure(r): + _hex = r.raw_hex # decode the twos complement a = twos_comp(unhex(_hex[0:2], 8)) b = twos_comp(unhex(_hex[2:4], 8)) v = ((a * 256.0) + b) / 4.0 - return Value(v, Unit.PA) + r.set(v, Unit.PA) # 0 to 327.675 kPa -def abs_evap_pressure(_hex): +def abs_evap_pressure(r): + _hex = r.raw_hex v = unhex(_hex) v = v / 200 - return Value(v, Unit.KPA) + r.set(v, Unit.KPA) # -32767 to 32768 Pa -def evap_pressure_alt(_hex): +def evap_pressure_alt(r): + _hex = r.raw_hex v = unhex(_hex) v = v - 32767 - return Value(v, Unit.PA) + r.set(v, Unit.PA) # 0 to 16,383.75 RPM -def rpm(_hex): +def rpm(r): + _hex = r.raw_hex v = unhex(_hex) v = v / 4.0 - return Value(v, Unit.RPM) + r.set(v, Unit.RPM) # 0 to 255 KPH -def speed(_hex): +def speed(r): + _hex = r.raw_hex v = unhex(_hex) - return Value(v, Unit.KPH) + r.set(v, Unit.KPH) # -64 to 63.5 degrees -def timing_advance(_hex): +def timing_advance(r): + _hex = r.raw_hex v = unhex(_hex) v = (v - 128) / 2.0 - return Value(v, Unit.DEGREES) + r.set(v, Unit.DEGREES) # -210 to 301 degrees -def inject_timing(_hex): +def inject_timing(r): + _hex = r.raw_hex v = unhex(_hex) v = (v - 26880) / 128.0 - return Value(v, Unit.DEGREES) + r.set(v, Unit.DEGREES) # 0 to 655.35 grams/sec -def maf(_hex): +def maf(r): + _hex = r.raw_hex v = unhex(_hex) v = v / 100.0 - return Value(v, Unit.GPS) + r.set(v, Unit.GPS) # 0 to 2550 grams/sec -def max_maf(_hex): +def max_maf(r): + _hex = r.raw_hex v = unhex(_hex[0:2]) v = v * 10 - return Value(v, Unit.GPS) + r.set(v, Unit.GPS) # 0 to 65535 seconds -def seconds(_hex): +def seconds(r): + _hex = r.raw_hex v = unhex(_hex) - return Value(v, Unit.SECONDS) + r.set(v, Unit.SECONDS) # 0 to 65535 minutes -def minutes(_hex): +def minutes(r): + _hex = r.raw_hex v = unhex(_hex) - return Value(v, Unit.MIN) + r.set(v, Unit.MIN) # 0 to 65535 km -def distance(_hex): +def distance(r): + _hex = r.raw_hex v = unhex(_hex) - return Value(v, Unit.KM) + r.set(v, Unit.KM) # 0 to 3212 Liters/hour -def fuel_rate(_hex): +def fuel_rate(r): + _hex = r.raw_hex v = unhex(_hex) v = v * 0.05 - return Value(v, Unit.LPH) + r.set(v, Unit.LPH) ''' @@ -175,7 +203,8 @@ def fuel_rate(_hex): -def status(_hex): +def status(r): + _hex = r.raw_hex bits = bitstring(_hex) output = {} @@ -222,7 +251,8 @@ def status(_hex): -def fuel_status(_hex): +def fuel_status(r): + _hex = r.raw_hex v = unhex(_hex) i = int(math.sqrt(v)) # only a single bit should be on @@ -231,10 +261,11 @@ def fuel_status(_hex): if i < len(FUEL_STATUS): v = FUEL_STATUS[i] - return Value(v, Unit.NONE) + r.set(v, Unit.NONE) -def air_status(_hex): +def air_status(r): + _hex = r.raw_hex v = unhex(_hex) i = int(math.sqrt(v)) # only a single bit should be on @@ -243,9 +274,10 @@ def air_status(_hex): if i < len(AIR_STATUS): v = AIR_STATUS[i] - return Value(v, Unit.NONE) + r.set(v, Unit.NONE) -def obd_compliance(_hex): +def obd_compliance(r): + _hex = r.raw_hex i = unhex(_hex) v = "Error: Unknown OBD compliance response" @@ -253,10 +285,11 @@ def obd_compliance(_hex): if i < len(OBD_COMPLIANCE): v = OBD_COMPLIANCE[i] - return Value(v, Unit.NONE) + r.set(v, Unit.NONE) -def fuel_type(_hex): +def fuel_type(r): + _hex = r.raw_hex i = unhex(_hex) v = "Error: Unknown fuel type response" @@ -264,7 +297,7 @@ def fuel_type(_hex): if i < len(FUEL_TYPES): v = FUEL_TYPES[i] - return Value(v, Unit.NONE) + r.set(v, Unit.NONE) # Get the description of a DTC def describeCode(code): @@ -275,10 +308,11 @@ def describeCode(code): if DTC.has_key(code): v = DTC[code] - return Value(v, Unit.NONE) + r.set(v, Unit.NONE) # converts 2 bytes of hex into a DTC code -def dtc(_hex): +def dtc(r): + _hex = r.raw_hex dtc = "" bits = bitstring(_hex[0]) @@ -286,10 +320,11 @@ def dtc(_hex): dtc += str(unbin(bits[2:4])) dtc += _hex[1:4] - return Value(dtc, Unit.NONE) + r.set(dtc, Unit.NONE) # converts a frame of 2-byte DTCs into a list of DTCs -def dtc_frame(_hex): +def dtc_frame(r): + _hex = r.raw_hex code_length = 4 # number of hex chars consumed by one code size = len(_hex / 4) # number of codes defined in THIS FRAME (not total) codes = [] @@ -300,4 +335,4 @@ def dtc_frame(_hex): codes.append(dtc(_hex[start:end])) - return Value(codes, Unit.NONE) + r.set(codes, Unit.NONE) diff --git a/obd/utils.py b/obd/utils.py index de5c5637..faa9fa5d 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -25,10 +25,15 @@ class Unit: LPH = "Liters per Hour" -class Value(): - def __init__(self, value, unit): +class Response(): + def __init__(self, raw_hex): + self.value = None + self.unit = Unit.NONE + self.raw_hex = raw_hex + + def set(self, value, unit): self.value = value - self.unit = unit + self.unit = unit def __str__(self): return "%s %s" % (str(self.value), str(self.unit)) From 8dd33171c79e853d0e6e3f5669fb90869f91fa9d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 18 Oct 2014 19:19:58 -0400 Subject: [PATCH 048/569] fixed decoders to operate of the constrained hex --- obd/commands.py | 5 ++- obd/decoders.py | 105 ++++++++++++++++-------------------------------- 2 files changed, 38 insertions(+), 72 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 754dfb3a..04b108e7 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -75,8 +75,9 @@ def compute(self, _hex): print "Receieved more data than expected, trying to parse anyways..." _hex = _hex[:diff] # chop off the right side to fit - # decoded value - self.decode(r) + # decoded value into the response object + # NOTE: the decoder does not operate off of the raw_hex + self.decode(_hex, r) return r diff --git a/obd/decoders.py b/obd/decoders.py index 02e7bf5a..bc3b86ec 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -5,18 +5,15 @@ # todo -def todo(r): - _hex = r.raw_hex +def todo(_hex, r): r.set(_hex, Unit.NONE) # hex in, hex out -def noop(r): - _hex = r.raw_hex +def noop(_hex, r): r.set(_hex, Unit.NONE) # hex in, bitstring out -def pid(r): - _hex = r.raw_hex +def pid(_hex, r): v = bitstring(_hex) r.set(v, Unit.NONE) @@ -25,90 +22,77 @@ def pid(r): Return Value object with value and units ''' -def count(r): - _hex = r.raw_hex +def count(_hex, r): v = unhex(_hex) r.set(v, Unit.COUNT) # 0 to 100 % -def percent(r): - _hex = r.raw_hex +def percent(_hex, r): v = unhex(_hex[0:2]) v = v * 100.0 / 255.0 r.set(v, Unit.PERCENT) # -100 to 100 % -def percent_centered(r): - _hex = r.raw_hex +def percent_centered(_hex, r): v = unhex(_hex[0:2]) v = (v - 128) * 100.0 / 128.0 r.set(v, Unit.PERCENT) # -40 to 215 C -def temp(r): - _hex = r.raw_hex +def temp(_hex, r): v = unhex(_hex) v = v - 40 r.set(v, Unit.C) # -40 to 6513.5 C -def catalyst_temp(r): - _hex = r.raw_hex +def catalyst_temp(_hex, r): v = unhex(_hex) v = (v / 10.0) - 40 r.set(v, Unit.C) # -128 to 128 mA -def current_centered(r): - _hex = r.raw_hex +def current_centered(_hex, r): v = unhex(_hex[4:8]) v = (v / 256.0) - 128 r.set(v, Unit.MA) # 0 to 1.275 volts -def sensor_voltage(r): - _hex = r.raw_hex +def sensor_voltage(_hex, r): v = unhex(_hex[0:2]) v = v / 200.0 r.set(v, Unit.VOLT) # 0 to 8 volts -def sensor_voltage_big(r): - _hex = r.raw_hex +def sensor_voltage_big(_hex, r): v = unhex(_hex[4:8]) v = (v * 8.0) / 65535 r.set(v, Unit.VOLT) # 0 to 765 kPa -def fuel_pressure(r): - _hex = r.raw_hex +def fuel_pressure(_hex, r): v = unhex(_hex) v = v * 3 r.set(v, Unit.KPA) # 0 to 255 kPa -def pressure(r): - _hex = r.raw_hex +def pressure(_hex, r): v = unhex(_hex) r.set(v, Unit.KPA) # 0 to 5177 kPa -def fuel_pres_vac(r): - _hex = r.raw_hex +def fuel_pres_vac(_hex, r): v = unhex(_hex) v = v * 0.079 r.set(v, Unit.KPA) # 0 to 655,350 kPa -def fuel_pres_direct(r): - _hex = r.raw_hex +def fuel_pres_direct(_hex, r): v = unhex(_hex) v = v * 10 r.set(v, Unit.KPA) # -8192 to 8192 Pa -def evap_pressure(r): - _hex = r.raw_hex +def evap_pressure(_hex, r): # decode the twos complement a = twos_comp(unhex(_hex[0:2], 8)) b = twos_comp(unhex(_hex[2:4], 8)) @@ -116,81 +100,69 @@ def evap_pressure(r): r.set(v, Unit.PA) # 0 to 327.675 kPa -def abs_evap_pressure(r): - _hex = r.raw_hex +def abs_evap_pressure(_hex, r): v = unhex(_hex) v = v / 200 r.set(v, Unit.KPA) # -32767 to 32768 Pa -def evap_pressure_alt(r): - _hex = r.raw_hex +def evap_pressure_alt(_hex, r): v = unhex(_hex) v = v - 32767 r.set(v, Unit.PA) # 0 to 16,383.75 RPM -def rpm(r): - _hex = r.raw_hex +def rpm(_hex, r): v = unhex(_hex) v = v / 4.0 r.set(v, Unit.RPM) # 0 to 255 KPH -def speed(r): - _hex = r.raw_hex +def speed(_hex, r): v = unhex(_hex) r.set(v, Unit.KPH) # -64 to 63.5 degrees -def timing_advance(r): - _hex = r.raw_hex +def timing_advance(_hex, r): v = unhex(_hex) v = (v - 128) / 2.0 r.set(v, Unit.DEGREES) # -210 to 301 degrees -def inject_timing(r): - _hex = r.raw_hex +def inject_timing(_hex, r): v = unhex(_hex) v = (v - 26880) / 128.0 r.set(v, Unit.DEGREES) # 0 to 655.35 grams/sec -def maf(r): - _hex = r.raw_hex +def maf(_hex, r): v = unhex(_hex) v = v / 100.0 r.set(v, Unit.GPS) # 0 to 2550 grams/sec -def max_maf(r): - _hex = r.raw_hex +def max_maf(_hex, r): v = unhex(_hex[0:2]) v = v * 10 r.set(v, Unit.GPS) # 0 to 65535 seconds -def seconds(r): - _hex = r.raw_hex +def seconds(_hex, r): v = unhex(_hex) r.set(v, Unit.SECONDS) # 0 to 65535 minutes -def minutes(r): - _hex = r.raw_hex +def minutes(_hex, r): v = unhex(_hex) r.set(v, Unit.MIN) # 0 to 65535 km -def distance(r): - _hex = r.raw_hex +def distance(_hex, r): v = unhex(_hex) r.set(v, Unit.KM) # 0 to 3212 Liters/hour -def fuel_rate(r): - _hex = r.raw_hex +def fuel_rate(_hex, r): v = unhex(_hex) v = v * 0.05 r.set(v, Unit.LPH) @@ -203,8 +175,7 @@ def fuel_rate(r): -def status(r): - _hex = r.raw_hex +def status(_hex, r): bits = bitstring(_hex) output = {} @@ -251,8 +222,7 @@ def status(r): -def fuel_status(r): - _hex = r.raw_hex +def fuel_status(_hex, r): v = unhex(_hex) i = int(math.sqrt(v)) # only a single bit should be on @@ -264,8 +234,7 @@ def fuel_status(r): r.set(v, Unit.NONE) -def air_status(r): - _hex = r.raw_hex +def air_status(_hex, r): v = unhex(_hex) i = int(math.sqrt(v)) # only a single bit should be on @@ -276,8 +245,7 @@ def air_status(r): r.set(v, Unit.NONE) -def obd_compliance(r): - _hex = r.raw_hex +def obd_compliance(_hex, r): i = unhex(_hex) v = "Error: Unknown OBD compliance response" @@ -288,8 +256,7 @@ def obd_compliance(r): r.set(v, Unit.NONE) -def fuel_type(r): - _hex = r.raw_hex +def fuel_type(_hex, r): i = unhex(_hex) v = "Error: Unknown fuel type response" @@ -311,8 +278,7 @@ def describeCode(code): r.set(v, Unit.NONE) # converts 2 bytes of hex into a DTC code -def dtc(r): - _hex = r.raw_hex +def dtc(_hex, r): dtc = "" bits = bitstring(_hex[0]) @@ -323,8 +289,7 @@ def dtc(r): r.set(dtc, Unit.NONE) # converts a frame of 2-byte DTCs into a list of DTCs -def dtc_frame(r): - _hex = r.raw_hex +def dtc_frame(_hex, r): code_length = 4 # number of hex chars consumed by one code size = len(_hex / 4) # number of codes defined in THIS FRAME (not total) codes = [] From 9fdce54ea0fddb5b6dcc4e5831825a7234ded605 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 18 Oct 2014 19:27:25 -0400 Subject: [PATCH 049/569] prevented decoders from touching the response object (for safety) --- obd/commands.py | 6 +- obd/decoders.py | 149 +++++++++++++++++++++++++----------------------- obd/utils.py | 44 +++++++------- 3 files changed, 104 insertions(+), 95 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 04b108e7..2965f1e8 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -76,9 +76,9 @@ def compute(self, _hex): _hex = _hex[:diff] # chop off the right side to fit # decoded value into the response object - # NOTE: the decoder does not operate off of the raw_hex - self.decode(_hex, r) - + # NOTE: the decoder does not operate off on the raw_hex + r.set(self.decode(_hex)) + return r def __str__(self): diff --git a/obd/decoders.py b/obd/decoders.py index bc3b86ec..00e398c5 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -3,169 +3,178 @@ from utils import * from codes import * +''' +All decoders take the form: + +def (_hex): + ... + return (, ) + +''' + # todo -def todo(_hex, r): - r.set(_hex, Unit.NONE) +def todo(_hex): + return (_hex, Unit.NONE) # hex in, hex out -def noop(_hex, r): - r.set(_hex, Unit.NONE) +def noop(_hex): + return (_hex, Unit.NONE) # hex in, bitstring out -def pid(_hex, r): +def pid(_hex): v = bitstring(_hex) - r.set(v, Unit.NONE) + return (v, Unit.NONE) ''' Sensor decoders Return Value object with value and units ''' -def count(_hex, r): +def count(_hex): v = unhex(_hex) - r.set(v, Unit.COUNT) + return (v, Unit.COUNT) # 0 to 100 % -def percent(_hex, r): +def percent(_hex): v = unhex(_hex[0:2]) v = v * 100.0 / 255.0 - r.set(v, Unit.PERCENT) + return (v, Unit.PERCENT) # -100 to 100 % -def percent_centered(_hex, r): +def percent_centered(_hex): v = unhex(_hex[0:2]) v = (v - 128) * 100.0 / 128.0 - r.set(v, Unit.PERCENT) + return (v, Unit.PERCENT) # -40 to 215 C -def temp(_hex, r): +def temp(_hex): v = unhex(_hex) v = v - 40 - r.set(v, Unit.C) + return (v, Unit.C) # -40 to 6513.5 C -def catalyst_temp(_hex, r): +def catalyst_temp(_hex): v = unhex(_hex) v = (v / 10.0) - 40 - r.set(v, Unit.C) + return (v, Unit.C) # -128 to 128 mA -def current_centered(_hex, r): +def current_centered(_hex): v = unhex(_hex[4:8]) v = (v / 256.0) - 128 - r.set(v, Unit.MA) + return (v, Unit.MA) # 0 to 1.275 volts -def sensor_voltage(_hex, r): +def sensor_voltage(_hex): v = unhex(_hex[0:2]) v = v / 200.0 - r.set(v, Unit.VOLT) + return (v, Unit.VOLT) # 0 to 8 volts -def sensor_voltage_big(_hex, r): +def sensor_voltage_big(_hex): v = unhex(_hex[4:8]) v = (v * 8.0) / 65535 - r.set(v, Unit.VOLT) + return (v, Unit.VOLT) # 0 to 765 kPa -def fuel_pressure(_hex, r): +def fuel_pressure(_hex): v = unhex(_hex) v = v * 3 - r.set(v, Unit.KPA) + return (v, Unit.KPA) # 0 to 255 kPa -def pressure(_hex, r): +def pressure(_hex): v = unhex(_hex) - r.set(v, Unit.KPA) + return (v, Unit.KPA) # 0 to 5177 kPa -def fuel_pres_vac(_hex, r): +def fuel_pres_vac(_hex): v = unhex(_hex) v = v * 0.079 - r.set(v, Unit.KPA) + return (v, Unit.KPA) # 0 to 655,350 kPa -def fuel_pres_direct(_hex, r): +def fuel_pres_direct(_hex): v = unhex(_hex) v = v * 10 - r.set(v, Unit.KPA) + return (v, Unit.KPA) # -8192 to 8192 Pa -def evap_pressure(_hex, r): +def evap_pressure(_hex): # decode the twos complement a = twos_comp(unhex(_hex[0:2], 8)) b = twos_comp(unhex(_hex[2:4], 8)) v = ((a * 256.0) + b) / 4.0 - r.set(v, Unit.PA) + return (v, Unit.PA) # 0 to 327.675 kPa -def abs_evap_pressure(_hex, r): +def abs_evap_pressure(_hex): v = unhex(_hex) v = v / 200 - r.set(v, Unit.KPA) + return (v, Unit.KPA) # -32767 to 32768 Pa -def evap_pressure_alt(_hex, r): +def evap_pressure_alt(_hex): v = unhex(_hex) v = v - 32767 - r.set(v, Unit.PA) + return (v, Unit.PA) # 0 to 16,383.75 RPM -def rpm(_hex, r): +def rpm(_hex): v = unhex(_hex) v = v / 4.0 - r.set(v, Unit.RPM) + return (v, Unit.RPM) # 0 to 255 KPH -def speed(_hex, r): +def speed(_hex): v = unhex(_hex) - r.set(v, Unit.KPH) + return (v, Unit.KPH) # -64 to 63.5 degrees -def timing_advance(_hex, r): +def timing_advance(_hex): v = unhex(_hex) v = (v - 128) / 2.0 - r.set(v, Unit.DEGREES) + return (v, Unit.DEGREES) # -210 to 301 degrees -def inject_timing(_hex, r): +def inject_timing(_hex): v = unhex(_hex) v = (v - 26880) / 128.0 - r.set(v, Unit.DEGREES) + return (v, Unit.DEGREES) # 0 to 655.35 grams/sec -def maf(_hex, r): +def maf(_hex): v = unhex(_hex) v = v / 100.0 - r.set(v, Unit.GPS) + return (v, Unit.GPS) # 0 to 2550 grams/sec -def max_maf(_hex, r): +def max_maf(_hex): v = unhex(_hex[0:2]) v = v * 10 - r.set(v, Unit.GPS) + return (v, Unit.GPS) # 0 to 65535 seconds -def seconds(_hex, r): +def seconds(_hex): v = unhex(_hex) - r.set(v, Unit.SECONDS) + return (v, Unit.SECONDS) # 0 to 65535 minutes -def minutes(_hex, r): +def minutes(_hex): v = unhex(_hex) - r.set(v, Unit.MIN) + return (v, Unit.MIN) # 0 to 65535 km -def distance(_hex, r): +def distance(_hex): v = unhex(_hex) - r.set(v, Unit.KM) + return (v, Unit.KM) # 0 to 3212 Liters/hour -def fuel_rate(_hex, r): +def fuel_rate(_hex): v = unhex(_hex) v = v * 0.05 - r.set(v, Unit.LPH) + return (v, Unit.LPH) ''' @@ -175,7 +184,7 @@ def fuel_rate(_hex, r): -def status(_hex, r): +def status(_hex): bits = bitstring(_hex) output = {} @@ -222,7 +231,7 @@ def status(_hex, r): -def fuel_status(_hex, r): +def fuel_status(_hex): v = unhex(_hex) i = int(math.sqrt(v)) # only a single bit should be on @@ -231,10 +240,10 @@ def fuel_status(_hex, r): if i < len(FUEL_STATUS): v = FUEL_STATUS[i] - r.set(v, Unit.NONE) + return (v, Unit.NONE) -def air_status(_hex, r): +def air_status(_hex): v = unhex(_hex) i = int(math.sqrt(v)) # only a single bit should be on @@ -243,9 +252,9 @@ def air_status(_hex, r): if i < len(AIR_STATUS): v = AIR_STATUS[i] - r.set(v, Unit.NONE) + return (v, Unit.NONE) -def obd_compliance(_hex, r): +def obd_compliance(_hex): i = unhex(_hex) v = "Error: Unknown OBD compliance response" @@ -253,10 +262,10 @@ def obd_compliance(_hex, r): if i < len(OBD_COMPLIANCE): v = OBD_COMPLIANCE[i] - r.set(v, Unit.NONE) + return (v, Unit.NONE) -def fuel_type(_hex, r): +def fuel_type(_hex): i = unhex(_hex) v = "Error: Unknown fuel type response" @@ -264,7 +273,7 @@ def fuel_type(_hex, r): if i < len(FUEL_TYPES): v = FUEL_TYPES[i] - r.set(v, Unit.NONE) + return (v, Unit.NONE) # Get the description of a DTC def describeCode(code): @@ -275,10 +284,10 @@ def describeCode(code): if DTC.has_key(code): v = DTC[code] - r.set(v, Unit.NONE) + return (v, Unit.NONE) # converts 2 bytes of hex into a DTC code -def dtc(_hex, r): +def dtc(_hex): dtc = "" bits = bitstring(_hex[0]) @@ -286,10 +295,10 @@ def dtc(_hex, r): dtc += str(unbin(bits[2:4])) dtc += _hex[1:4] - r.set(dtc, Unit.NONE) + return (dtc, Unit.NONE) # converts a frame of 2-byte DTCs into a list of DTCs -def dtc_frame(_hex, r): +def dtc_frame(_hex): code_length = 4 # number of hex chars consumed by one code size = len(_hex / 4) # number of codes defined in THIS FRAME (not total) codes = [] @@ -300,4 +309,4 @@ def dtc_frame(_hex, r): codes.append(dtc(_hex[start:end])) - r.set(codes, Unit.NONE) + return (codes, Unit.NONE) diff --git a/obd/utils.py b/obd/utils.py index faa9fa5d..f1490731 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -4,25 +4,25 @@ class Unit: - NONE = None - RATIO = "Ratio" - COUNT = "Count" - PERCENT = "Percent" - VOLT = "Volt" - F = "F" - C = "C" - SEC = "Second" - MIN = "Minute" - PA = "Pa" - KPA = "kPa" - PSI = "PSI" - KPH = "Kilometers per Hour" - MPH = "Miles per Hour" - DEGREES = "Degrees" - GPS = "Grams per Second" - MA = "mA" - KM = "km" - LPH = "Liters per Hour" + NONE = None + RATIO = "Ratio" + COUNT = "Count" + PERCENT = "Percent" + VOLT = "Volt" + F = "F" + C = "C" + SEC = "Second" + MIN = "Minute" + PA = "Pa" + KPA = "kPa" + PSI = "PSI" + KPH = "Kilometers per Hour" + MPH = "Miles per Hour" + DEGREES = "Degrees" + GPS = "Grams per Second" + MA = "mA" + KM = "km" + LPH = "Liters per Hour" class Response(): @@ -31,9 +31,9 @@ def __init__(self, raw_hex): self.unit = Unit.NONE self.raw_hex = raw_hex - def set(self, value, unit): - self.value = value - self.unit = unit + def set(self, decode): + self.value = decode[0] + self.unit = decode[1] def __str__(self): return "%s %s" % (str(self.value), str(self.unit)) From 260af5a5e63137c62794696a71b669dc1665f728 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 18 Oct 2014 19:50:56 -0400 Subject: [PATCH 050/569] misc bugfixes --- obd/commands.py | 4 ++-- obd/obd.py | 26 +++++++++++++++----------- obd/port.py | 4 ++-- obd/utils.py | 5 ++++- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 2965f1e8..e39e0a05 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -25,7 +25,7 @@ from decoders import * -from utils import Response, Unit +from utils import Response @@ -267,7 +267,7 @@ def pid_getters(self): for m in self.modes: for c in m: if c.decode == pid: # GET commands have a special decoder - getter.append(c) + getters.append(c) return getters # sets the boolean for diff --git a/obd/obd.py b/obd/obd.py index 6d932947..64a1c6df 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -4,7 +4,7 @@ from port import OBDPort, State from commands import commands -from utils import scanSerial +from utils import scanSerial, Response @@ -58,18 +58,21 @@ def load_commands(self): # GET commands should sequentialy turn themselves on (become marked as supported) # MODE 1 PID 0 is marked supported by default if self.has_command(get): - supported = self.query(get).value # string of binary 01010101010101 + response = self.query(get) - # loop through PIDs binary - for i in range(len(supported)): - if supported[i] == "1": + if not response.isEmpty(): + supported = response.value # string of binary 01010101010101 - mode = get.getModeInt() - pid = get.getPidInt() + i + 1 + # loop through PIDs binary + for i in range(len(supported)): + if supported[i] == "1": - c = commands[mode][pid] - c.supported = True - self.supportedCommands.append(c) + mode = get.getModeInt() + pid = get.getPidInt() + i + 1 + + c = commands[mode][pid] + c.supported = True + self.supportedCommands.append(c) def print_commands(self): @@ -83,7 +86,8 @@ def query(self, command): if self.has_command(command): return self.port.get_sensor_value(command) else: - + print "'%s' is not supported" % str(command) + return Response() # return empty response diff --git a/obd/port.py b/obd/port.py index 403c3d64..8c51f097 100644 --- a/obd/port.py +++ b/obd/port.py @@ -25,7 +25,7 @@ import serial import string import time -from utils import unhex +from utils import Response, unhex GET_DTC_COMMAND = "03" @@ -192,7 +192,7 @@ def get_sensor_value(self, command): if data != "NODATA": data = command.compute(data) else: - return "NORESPONSE" + data = Response() return data diff --git a/obd/utils.py b/obd/utils.py index f1490731..56d45c41 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -26,11 +26,14 @@ class Unit: class Response(): - def __init__(self, raw_hex): + def __init__(self, raw_hex=""): self.value = None self.unit = Unit.NONE self.raw_hex = raw_hex + def isEmpty(self): + return self.value == None + def set(self, decode): self.value = decode[0] self.unit = decode[1] From a4cae612751d92177ac581402b288bdc0c5ab1e9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 18 Oct 2014 19:54:48 -0400 Subject: [PATCH 051/569] using continue vs nested if statements --- obd/obd.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/obd/obd.py b/obd/obd.py index 64a1c6df..53f9bbae 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -57,22 +57,26 @@ def load_commands(self): for get in pid_getters: # GET commands should sequentialy turn themselves on (become marked as supported) # MODE 1 PID 0 is marked supported by default - if self.has_command(get): - response = self.query(get) + if not self.has_command(get): + continue - if not response.isEmpty(): - supported = response.value # string of binary 01010101010101 + response = self.query(get) # ask nicely - # loop through PIDs binary - for i in range(len(supported)): - if supported[i] == "1": + if response.isEmpty(): + continue + + supported = response.value # string of binary 01010101010101 - mode = get.getModeInt() - pid = get.getPidInt() + i + 1 + # loop through PIDs binary + for i in range(len(supported)): + if supported[i] == "1": - c = commands[mode][pid] - c.supported = True - self.supportedCommands.append(c) + mode = get.getModeInt() + pid = get.getPidInt() + i + 1 + + c = commands[mode][pid] + c.supported = True + self.supportedCommands.append(c) def print_commands(self): From e0c7d03d018c757d0975f8af374ddf080a3492df Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 19 Oct 2014 14:55:07 -0400 Subject: [PATCH 052/569] attempting to connect to car --- obd/commands.py | 17 +++++++++-------- obd/port.py | 8 +------- obd/utils.py | 6 +++--- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index e39e0a05..e1b178e7 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -57,27 +57,28 @@ def getModeInt(self): def getPidInt(self): return unhex(self.pid) - def compute(self, _hex): + def compute(self, _data): # create the response object with the raw hex recieved - r = Response(_hex) + r = Response(_data) + print "RX: ", _data - if "NODATA" not in _hex: + if "NODATA" not in _data: # constrain number of bytes in response if (self.bytes > 0): # zero bytes means flexible response - diff = (self.bytes * 2) - len(_hex) # length discrepency in number of hex digits + diff = (self.bytes * 2) - len(_data) # length discrepency in number of hex digits if diff > 0: print "Receieved less data than expected, trying to parse anyways..." - _hex += ('0' * diff) # pad the right side with zeros + _data += ('0' * diff) # pad the right side with zeros elif diff < 0: print "Receieved more data than expected, trying to parse anyways..." - _hex = _hex[:diff] # chop off the right side to fit + _data = _data[:diff] # chop off the right side to fit # decoded value into the response object - # NOTE: the decoder does not operate off on the raw_hex - r.set(self.decode(_hex)) + # NOTE: the decoder does not operate off on the raw_data + r.set(self.decode(_data)) return r diff --git a/obd/port.py b/obd/port.py index 8c51f097..3f20728d 100644 --- a/obd/port.py +++ b/obd/port.py @@ -28,11 +28,6 @@ from utils import Response, unhex -GET_DTC_COMMAND = "03" -CLEAR_DTC_COMMAND = "04" -GET_FREEZE_DTC_COMMAND = "07" - - class State(): """ Enum for connection states """ Unconnected, Connected = range(2) @@ -187,12 +182,11 @@ def get_sensor_value(self, command): data = self.get_result() if data: - print "RX: ", data data = self.interpret_result(data) if data != "NODATA": data = command.compute(data) else: - data = Response() + data = Response() # return empty response return data diff --git a/obd/utils.py b/obd/utils.py index 56d45c41..165fa267 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -26,13 +26,13 @@ class Unit: class Response(): - def __init__(self, raw_hex=""): + def __init__(self, raw_data=""): self.value = None self.unit = Unit.NONE - self.raw_hex = raw_hex + self.raw_data = raw_data def isEmpty(self): - return self.value == None + return (self.value == None) or (len(self.raw_data) == 0) def set(self, decode): self.value = decode[0] From 22c68988d186a8d60cb8ed61beb5b65ae62455e1 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 19 Oct 2014 15:03:18 -0400 Subject: [PATCH 053/569] restored old get_result function --- obd/obd.py | 1 + obd/port.py | 39 +++++++++++++++++++++++++++------------ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/obd/obd.py b/obd/obd.py index 53f9bbae..c6475748 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -87,6 +87,7 @@ def has_command(self, c): return c.supported def query(self, command): + print "TX: " + str(command) if self.has_command(command): return self.port.get_sensor_value(command) else: diff --git a/obd/port.py b/obd/port.py index 3f20728d..6f15a303 100644 --- a/obd/port.py +++ b/obd/port.py @@ -160,19 +160,36 @@ def interpret_result(self,code): return code def get_result(self): - + """Internal use only: not a public interface""" + #time.sleep(0.01) + repeat_count = 0 if self.port is not None: - result = "" + buffer = "" while 1: c = self.port.read(1) - if not c or c == ">": - break - if c == "\x00": + if len(c) == 0: + if(repeat_count == 5): + break + print "Got nothing\n" + repeat_count = repeat_count + 1 + continue + + if c == '\r': continue - result += c - return result + + if c == ">": + break; + + if buffer != "" or c != ">": #if something is in buffer, add everything + buffer = buffer + c + + #print "Get result:" + buffer + if(buffer == ""): + return None + return buffer else: - return "NORESPONSE" + print "NO self.port!" + return None # get sensor value from command def get_sensor_value(self, command): @@ -183,12 +200,10 @@ def get_sensor_value(self, command): if data: data = self.interpret_result(data) - if data != "NODATA": - data = command.compute(data) + return command.compute(data) else: - data = Response() # return empty response + return Response() # return empty response - return data # # fixme: j1979 specifies that the program should poll until the number From 10af4504bd310529e2c09e60dcb7530b92e47a03 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 19 Oct 2014 15:24:55 -0400 Subject: [PATCH 054/569] moved preprocessing to OBDCommand's compute() function --- obd/commands.py | 6 ++++-- obd/port.py | 33 ++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index e1b178e7..970a2e6a 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -58,11 +58,13 @@ def getPidInt(self): return unhex(self.pid) def compute(self, _data): - # create the response object with the raw hex recieved + # create the response object with the raw data recieved r = Response(_data) - print "RX: ", _data + + _data = "".join(_data.split()) # strips spaces, and removes [\n\r\t] if "NODATA" not in _data: + _data = _data[4:] # the first 4 chars are codes from the ELM (we don't need those) # constrain number of bytes in response if (self.bytes > 0): # zero bytes means flexible response diff --git a/obd/port.py b/obd/port.py index 6f15a303..3bc247a4 100644 --- a/obd/port.py +++ b/obd/port.py @@ -159,37 +159,40 @@ def interpret_result(self,code): code = code[4:] return code + def get_result(self): """Internal use only: not a public interface""" - #time.sleep(0.01) - repeat_count = 0 + + attempts = 5 + result = "" + if self.port is not None: - buffer = "" while 1: c = self.port.read(1) + + # if nothing was recieved if len(c) == 0: - if(repeat_count == 5): + + if(attempts <= 0): break + print "Got nothing\n" - repeat_count = repeat_count + 1 + attempts -= 1 continue + # skip carraige returns if c == '\r': continue + # end on chevron if c == ">": break; - - if buffer != "" or c != ">": #if something is in buffer, add everything - buffer = buffer + c - - #print "Get result:" + buffer - if(buffer == ""): - return None - return buffer + else: # whatever is left must be part of the response + result = result + c else: print "NO self.port!" - return None + + return result # get sensor value from command def get_sensor_value(self, command): @@ -199,7 +202,7 @@ def get_sensor_value(self, command): data = self.get_result() if data: - data = self.interpret_result(data) + # data = self.interpret_result(data) return command.compute(data) else: return Response() # return empty response From c989b0a518ff838ce7ddf507a0099e76d8fb1ebc Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 19 Oct 2014 15:42:56 -0400 Subject: [PATCH 055/569] successful test with sensor data --- obd/commands.py | 14 +++++--------- obd/port.py | 9 ++++----- obd/utils.py | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 970a2e6a..90d42a3a 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -58,6 +58,10 @@ def getPidInt(self): return unhex(self.pid) def compute(self, _data): + # _data will be the string returned from the device. + # It should look something like this: + # '41 11 0 0\r\r' + # create the response object with the raw data recieved r = Response(_data) @@ -68,15 +72,7 @@ def compute(self, _data): # constrain number of bytes in response if (self.bytes > 0): # zero bytes means flexible response - - diff = (self.bytes * 2) - len(_data) # length discrepency in number of hex digits - - if diff > 0: - print "Receieved less data than expected, trying to parse anyways..." - _data += ('0' * diff) # pad the right side with zeros - elif diff < 0: - print "Receieved more data than expected, trying to parse anyways..." - _data = _data[:diff] # chop off the right side to fit + constrainHex(_data, self.bytes) # decoded value into the response object # NOTE: the decoder does not operate off on the raw_data diff --git a/obd/port.py b/obd/port.py index 3bc247a4..a73678b5 100644 --- a/obd/port.py +++ b/obd/port.py @@ -85,6 +85,9 @@ def __init__(self, portname): print "atz response:" + self.ELMver self.send_command("ate0") # echo off print "ate0 response:" + self.get_result() + + + ''' self.send_command("0100") ready = self.get_result() @@ -93,7 +96,7 @@ def __init__(self, portname): return print "0100 response:" + ready - + ''' def error(self, msg=None): """ called when connection error has been encountered """ @@ -134,10 +137,6 @@ def send_command(self, cmd): def interpret_result(self,code): - # Code will be the string returned from the device. - # It should look something like this: - # '41 11 0 0\r\r' - # 9 seems to be the length of the shortest valid response if len(code) < 7: #raise Exception("BogusCode") diff --git a/obd/utils.py b/obd/utils.py index 165fa267..2a239962 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -8,6 +8,7 @@ class Unit: RATIO = "Ratio" COUNT = "Count" PERCENT = "Percent" + RPM = "RPM" VOLT = "Volt" F = "F" C = "C" @@ -73,6 +74,20 @@ def twos_comp(val, num_bits): val = val - (1< 0: + print "Receieved less data than expected, trying to parse anyways..." + _hex += ('0' * diff) # pad the right side with zeros + elif diff < 0: + print "Receieved more data than expected, trying to parse anyways..." + _hex = _hex[:diff] # chop off the right side to fit + + return _hex + + def tryPort(portStr): """returns boolean for port availability""" try: From b6fd8ccac7eab4049ace54f35f47bc4ba1edb99d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 19 Oct 2014 16:06:36 -0400 Subject: [PATCH 056/569] removed old code from port class --- obd/commands.py | 19 ++++++----- obd/obd.py | 19 +++++++---- obd/port.py | 91 +++++++++++++------------------------------------ obd/utils.py | 2 +- 4 files changed, 48 insertions(+), 83 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 90d42a3a..5a0bf091 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -35,6 +35,7 @@ def __init__(self, name, desc, mode, pid, returnBytes, decoder, supported=False) self.desc = desc self.mode = mode self.pid = pid + self.hex = mode + pid # the actual command transmitted to the port self.bytes = returnBytes # number of bytes expected in return self.decode = decoder self.supported = supported @@ -48,9 +49,6 @@ def clone(self): self.decode, self.supported) - def getCommand(self): - return self.mode + self.pid - def getModeInt(self): return unhex(self.mode) @@ -59,24 +57,27 @@ def getPidInt(self): def compute(self, _data): # _data will be the string returned from the device. - # It should look something like this: - # '41 11 0 0\r\r' + # It should look something like this: '41 11 0 0\r\r' # create the response object with the raw data recieved r = Response(_data) - _data = "".join(_data.split()) # strips spaces, and removes [\n\r\t] + # strips spaces, and removes [\n\r\t] + _data = "".join(_data.split()) + + if (len(_data) > 0) and ("NODATA" not in _data) and isHex(_data): - if "NODATA" not in _data: - _data = _data[4:] # the first 4 chars are codes from the ELM (we don't need those) + # the first 4 chars are codes from the ELM (we don't need those) + _data = _data[4:] # constrain number of bytes in response if (self.bytes > 0): # zero bytes means flexible response constrainHex(_data, self.bytes) # decoded value into the response object - # NOTE: the decoder does not operate off on the raw_data r.set(self.decode(_data)) + else: + pass # not a parseable response return r diff --git a/obd/obd.py b/obd/obd.py index c6475748..753a2dbc 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -38,9 +38,9 @@ def connect(self, portstr=None): return self.is_connected() - + # checks the port state for conncetion status def is_connected(self): - return (self.port is not None) and (self.port.state == State.Connected) + return (self.port is not None) and self.port.is_connected() def get_port_name(self): @@ -86,10 +86,17 @@ def print_commands(self): def has_command(self, c): return c.supported - def query(self, command): - print "TX: " + str(command) - if self.has_command(command): - return self.port.get_sensor_value(command) + def query(self, command, force=False): + #print "TX: " + str(command) + + if self.has_command(command) and not force: + + # send command to the port + self.port.send(command.hex) + + # get the data, and compute a response object + return command.compute(self.port.get()) + else: print "'%s' is not supported" % str(command) return Response() # return empty response diff --git a/obd/port.py b/obd/port.py index a73678b5..fe5a8a9b 100644 --- a/obd/port.py +++ b/obd/port.py @@ -68,35 +68,27 @@ def __init__(self, portname): return print "Interface successfully opened on " + self.get_port_name() - print "Connecting to ECU..." try: - self.send_command("atz") # initialize + self.send("atz") # initialize time.sleep(1) + self.ELMver = self.get() + + if self.ELMver is None : + self.error("ELMver did not return") + return + + print "atz response: " + self.ELMver + except serial.SerialException as e: self.error(e) return - self.ELMver = self.get_result() - if self.ELMver is None : - self.error("ELMver returned None") - return - - print "atz response:" + self.ELMver - self.send_command("ate0") # echo off - print "ate0 response:" + self.get_result() - + self.send("ate0") # echo off + print "ate0 response: " + self.get() - ''' - self.send_command("0100") - ready = self.get_result() + print "Connected to ECU" - if ready is None: - self.state = State.Unconnected - return - - print "0100 response:" + ready - ''' def error(self, msg=None): """ called when connection error has been encountered """ @@ -114,52 +106,31 @@ def error(self, msg=None): def get_port_name(self): return self.port.portstr if (self.port is not None) else "No Port" + def is_connected(self): + return self.state == State.Connected def close(self): """ Resets device and closes all associated filehandles""" - if (self.port != None) and self.state == State.Connected: - self.send_command("atz") + if (self.port != None) and (self.state == State.Connected): + self.send("atz") self.port.close() self.port = None self.ELMver = "Unknown" - def send_command(self, cmd): + # sends the hex string to the port + def send(self, cmd): if self.port: self.port.flushOutput() self.port.flushInput() for c in cmd: self.port.write(c) self.port.write("\r\n") - #print "Send command:" + cmd - - def interpret_result(self,code): - - # 9 seems to be the length of the shortest valid response - if len(code) < 7: - #raise Exception("BogusCode") - print "boguscode?"+code - - # get the first thing returned, echo should be off - code = string.split(code, "\r") - code = code[0] - #remove whitespace - code = string.split(code) - code = string.join(code, "") - - #cables can behave differently - if code[:6] == "NODATA": # there is no such sensor - return "NODATA" - - # first 4 characters are code from ELM - code = code[4:] - return code - - - def get_result(self): + # accumulates and returns the ports response + def get(self): """Internal use only: not a public interface""" attempts = 5 @@ -193,20 +164,6 @@ def get_result(self): return result - # get sensor value from command - def get_sensor_value(self, command): - - cmd = command.getCommand() - self.send_command(cmd) - data = self.get_result() - - if data: - # data = self.interpret_result(data) - return command.compute(data) - else: - return Response() # return empty response - - # # fixme: j1979 specifies that the program should poll until the number # of returned DTCs matches the number indicated by a call to PID 01 @@ -224,8 +181,8 @@ def get_dtc(self): print "Number of stored DTC:" + str(dtcNumber) + " MIL: " + str(mil) # get all DTC, 3 per mesg response for i in range(0, ((dtcNumber+2)/3)): - self.send_command(GET_DTC_COMMAND) - res = self.get_result() + self.send(GET_DTC_COMMAND) + res = self.get() print "DTC result:" + res for i in range(0, 3): val1 = unhex(res[3+i*6:5+i*6]) @@ -239,8 +196,8 @@ def get_dtc(self): DTCCodes.append(["Active",DTCStr]) #read mode 7 - self.send_command(GET_FREEZE_DTC_COMMAND) - res = self.get_result() + self.send(GET_FREEZE_DTC_COMMAND) + res = self.get() if res[:7] == "NODATA": #no freeze frame return DTCCodes diff --git a/obd/utils.py b/obd/utils.py index 2a239962..4b438418 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -28,7 +28,7 @@ class Unit: class Response(): def __init__(self, raw_data=""): - self.value = None + self.value = "No Data" self.unit = Unit.NONE self.raw_data = raw_data From 5b78199e26c96a005f1f6fc08580cf9166394e20 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 19 Oct 2014 16:10:23 -0400 Subject: [PATCH 057/569] moved computation to constructor of command object --- obd/commands.py | 17 ++++++++--------- obd/obd.py | 4 ++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 5a0bf091..6df2e528 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -35,10 +35,17 @@ def __init__(self, name, desc, mode, pid, returnBytes, decoder, supported=False) self.desc = desc self.mode = mode self.pid = pid - self.hex = mode + pid # the actual command transmitted to the port self.bytes = returnBytes # number of bytes expected in return self.decode = decoder self.supported = supported + + # computed properties + self.hex = mode + pid # the actual command transmitted to the port + self.mode_int = unhex(self.mode) + self.pid_int = unhex(self.pid) + + def __str__(self): + return self.desc def clone(self): return OBDCommand(self.name, @@ -49,12 +56,6 @@ def clone(self): self.decode, self.supported) - def getModeInt(self): - return unhex(self.mode) - - def getPidInt(self): - return unhex(self.pid) - def compute(self, _data): # _data will be the string returned from the device. # It should look something like this: '41 11 0 0\r\r' @@ -81,8 +82,6 @@ def compute(self, _data): return r - def __str__(self): - return self.desc diff --git a/obd/obd.py b/obd/obd.py index 753a2dbc..58d5a41a 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -71,8 +71,8 @@ def load_commands(self): for i in range(len(supported)): if supported[i] == "1": - mode = get.getModeInt() - pid = get.getPidInt() + i + 1 + mode = get.mode_int + pid = get.pid_int + i + 1 c = commands[mode][pid] c.supported = True From 5e97aff02b801ab0a3414e4473d90312d2b2d018 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 19 Oct 2014 16:16:34 -0400 Subject: [PATCH 058/569] added command checking to PID loader --- obd/commands.py | 10 ++++++++++ obd/obd.py | 9 +++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 6df2e528..7799fc84 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -277,5 +277,15 @@ def set_supported(self, mode, pid, v): else: print "set_supported only accepts boolean values" + # checks for existance of int mode and int pid + def has(self, mode, pid): + if (mode < 0) or (pid < 0): + return False + if mode >= len(self.modes): + return False + if pid >= len(self.modes[mode]): + return False + return True + # export this object commands = Commands() diff --git a/obd/obd.py b/obd/obd.py index 58d5a41a..8cbde161 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -74,9 +74,10 @@ def load_commands(self): mode = get.mode_int pid = get.pid_int + i + 1 - c = commands[mode][pid] - c.supported = True - self.supportedCommands.append(c) + if commands.has(mode, pid) + c = commands[mode][pid] + c.supported = True + self.supportedCommands.append(c) def print_commands(self): @@ -84,7 +85,7 @@ def print_commands(self): print str(c) def has_command(self, c): - return c.supported + return commands.has(c.mode_int, c.pid_int) and c.supported def query(self, command, force=False): #print "TX: " + str(command) From b0acdd5720d457d7e8abb813e72e60b109363d1c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 19 Oct 2014 19:48:44 -0400 Subject: [PATCH 059/569] fixed fuel status bit check --- obd/commands.py | 2 +- obd/decoders.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 90d42a3a..24de1b02 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -101,7 +101,7 @@ def __str__(self): OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , "01", "04", 1, percent ), OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05", 1, temp ), OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06", 1, percent_centered ), - OBDCommand("Long_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07", 1, percent_centered ), + OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07", 1, percent_centered ), OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08", 1, percent_centered ), OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09", 1, percent_centered ), OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A", 1, fuel_pressure ), diff --git a/obd/decoders.py b/obd/decoders.py index 00e398c5..2e306263 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -210,7 +210,7 @@ def status(_hex): if(output["Ignition Type"] == IGNITION_TYPE[0]): # spark for i in range(8): if SPARK_TESTS[i] is not None: - + print i t = Test(SPARK_TESTS[i], \ bitToBool(bits[(2 * 8) + i]), \ bitToBool(bits[(3 * 8) + i])) @@ -232,8 +232,8 @@ def status(_hex): def fuel_status(_hex): - v = unhex(_hex) - i = int(math.sqrt(v)) # only a single bit should be on + v = unhex(_hex[0:2]) + i = int(math.log(v, 2)) # only a single bit should be on v = "Error: Unknown fuel status response" From 37b482f8d4a0147bb2078a7d3623f0d02b3e221a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 23 Oct 2014 13:48:10 -0400 Subject: [PATCH 060/569] minor fixes for decoders and force commands --- obd/decoders.py | 2 +- obd/obd.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 2e306263..1f3b466f 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -245,7 +245,7 @@ def fuel_status(_hex): def air_status(_hex): v = unhex(_hex) - i = int(math.sqrt(v)) # only a single bit should be on + i = int(math.log(v, 2)) # only a single bit should be on v = "Error: Unknown air status response" diff --git a/obd/obd.py b/obd/obd.py index 8cbde161..f1edbc1e 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -90,7 +90,7 @@ def has_command(self, c): def query(self, command, force=False): #print "TX: " + str(command) - if self.has_command(command) and not force: + if self.has_command(command) or force: # send command to the port self.port.send(command.hex) From 7b9011a4edaefb9985c1c879e97c704b6791146b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 23 Oct 2014 13:50:58 -0400 Subject: [PATCH 061/569] comments --- obd/decoders.py | 6 ++++++ obd/port.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/obd/decoders.py b/obd/decoders.py index 1f3b466f..eced6086 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -286,6 +286,12 @@ def describeCode(code): return (v, Unit.NONE) + + +''' +The following decoders are untested due to lack of a broken car +''' + # converts 2 bytes of hex into a DTC code def dtc(_hex): dtc = "" diff --git a/obd/port.py b/obd/port.py index fe5a8a9b..1a557b03 100644 --- a/obd/port.py +++ b/obd/port.py @@ -168,6 +168,7 @@ def get(self): # fixme: j1979 specifies that the program should poll until the number # of returned DTCs matches the number indicated by a call to PID 01 # + ''' def get_dtc(self): """Returns a list of all pending DTC codes. Each element consists of a 2-tuple: (DTC code (string), Code description (string) )""" @@ -215,3 +216,4 @@ def get_dtc(self): DTCCodes.append(["Passive",DTCStr]) return DTCCodes + ''' From cb7a551106aba56efa96ebb8c594626a89fad2ea Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 23 Oct 2014 21:37:08 -0400 Subject: [PATCH 062/569] fixed hex testing and removed precomputation from OBDCommand constructor --- obd/commands.py | 18 +++++++++++------- obd/obd.py | 10 +++++----- obd/utils.py | 5 +++++ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index e1da5d06..5550c1a1 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -25,7 +25,7 @@ from decoders import * -from utils import Response +from utils import * @@ -38,14 +38,9 @@ def __init__(self, name, desc, mode, pid, returnBytes, decoder, supported=False) self.bytes = returnBytes # number of bytes expected in return self.decode = decoder self.supported = supported - - # computed properties - self.hex = mode + pid # the actual command transmitted to the port - self.mode_int = unhex(self.mode) - self.pid_int = unhex(self.pid) def __str__(self): - return self.desc + return "%s%s: %s" % (self.mode, self.pid, self.desc) def clone(self): return OBDCommand(self.name, @@ -56,6 +51,15 @@ def clone(self): self.decode, self.supported) + def get_command(self): + return self.mode + self.pid # the actual command transmitted to the port + + def get_mode_int(self): + return unhex(self.mode) + + def get_pid_int(self): + return unhex(self.pid) + def compute(self, _data): # _data will be the string returned from the device. # It should look something like this: '41 11 0 0\r\r' diff --git a/obd/obd.py b/obd/obd.py index f1edbc1e..cf553060 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -71,10 +71,10 @@ def load_commands(self): for i in range(len(supported)): if supported[i] == "1": - mode = get.mode_int - pid = get.pid_int + i + 1 + mode = get.get_mode_int() + pid = get.get_pid_int() + i + 1 - if commands.has(mode, pid) + if commands.has(mode, pid): c = commands[mode][pid] c.supported = True self.supportedCommands.append(c) @@ -85,7 +85,7 @@ def print_commands(self): print str(c) def has_command(self, c): - return commands.has(c.mode_int, c.pid_int) and c.supported + return commands.has(c.get_mode_int(), c.get_pid_int()) and c.supported def query(self, command, force=False): #print "TX: " + str(command) @@ -93,7 +93,7 @@ def query(self, command, force=False): if self.has_command(command) or force: # send command to the port - self.port.send(command.hex) + self.port.send(command.get_command()) # get the data, and compute a response object return command.compute(self.port.get()) diff --git a/obd/utils.py b/obd/utils.py index 4b438418..e5a71875 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -1,5 +1,6 @@ import serial import errno +import string @@ -57,6 +58,7 @@ def __str__(self): def unhex(_hex): + _hex = "0" if _hex == "" else _hex return int(_hex, 16) def unbin(_bin): @@ -74,6 +76,9 @@ def twos_comp(val, num_bits): val = val - (1< Date: Thu, 23 Oct 2014 21:48:23 -0400 Subject: [PATCH 063/569] added missing commas in code list --- obd/codes.py | 126 +++++++++++++++++++++++++-------------------------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/obd/codes.py b/obd/codes.py index c2a44905..c148050a 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -2095,7 +2095,7 @@ IGNITION_TYPE = [ "Spark", - "Compression" + "Compression", ] SPARK_TESTS = [ @@ -2129,72 +2129,72 @@ ] AIR_STATUS = [ - "Upstream" - "Downstream of catalytic converter" - "From the outside atmosphere or off" - "Pump commanded on for diagnostics" + "Upstream", + "Downstream of catalytic converter", + "From the outside atmosphere or off", + "Pump commanded on for diagnostics", ] OBD_COMPLIANCE = [ - "Undefined" - "OBD-II as defined by the CARB" - "OBD as defined by the EPA" - "OBD and OBD-II" - "OBD-I" - "Not OBD compliant" - "EOBD (Europe)" - "EOBD and OBD-II" - "EOBD and OBD" - "EOBD, OBD and OBD II" - "JOBD (Japan)" - "JOBD and OBD II" - "JOBD and EOBD" - "JOBD, EOBD, and OBD II" - "Reserved" - "Reserved" - "Reserved" - "Engine Manufacturer Diagnostics (EMD)" - "Engine Manufacturer Diagnostics Enhanced (EMD+)" - "Heavy Duty On-Board Diagnostics (Child/Partial) (HD OBD-C)" - "Heavy Duty On-Board Diagnostics (HD OBD)" - "World Wide Harmonized OBD (WWH OBD)" - "Reserved" - "Heavy Duty Euro OBD Stage I without NOx control (HD EOBD-I)" - "Heavy Duty Euro OBD Stage I with NOx control (HD EOBD-I N)" - "Heavy Duty Euro OBD Stage II without NOx control (HD EOBD-II)" - "Heavy Duty Euro OBD Stage II with NOx control (HD EOBD-II N)" - "Reserved" - "Brazil OBD Phase 1 (OBDBr-1)" - "Brazil OBD Phase 2 (OBDBr-2)" - "Korean OBD (KOBD)" - "India OBD I (IOBD I)" - "India OBD II (IOBD II)" - "Heavy Duty Euro OBD Stage VI (HD EOBD-IV)" + "Undefined", + "OBD-II as defined by the CARB", + "OBD as defined by the EPA", + "OBD and OBD-II", + "OBD-I", + "Not OBD compliant", + "EOBD (Europe)", + "EOBD and OBD-II", + "EOBD and OBD", + "EOBD, OBD and OBD II", + "JOBD (Japan)", + "JOBD and OBD II", + "JOBD and EOBD", + "JOBD, EOBD, and OBD II", + "Reserved", + "Reserved", + "Reserved", + "Engine Manufacturer Diagnostics (EMD)", + "Engine Manufacturer Diagnostics Enhanced (EMD+)", + "Heavy Duty On-Board Diagnostics (Child/Partial) (HD OBD-C)", + "Heavy Duty On-Board Diagnostics (HD OBD)", + "World Wide Harmonized OBD (WWH OBD)", + "Reserved", + "Heavy Duty Euro OBD Stage I without NOx control (HD EOBD-I)", + "Heavy Duty Euro OBD Stage I with NOx control (HD EOBD-I N)", + "Heavy Duty Euro OBD Stage II without NOx control (HD EOBD-II)", + "Heavy Duty Euro OBD Stage II with NOx control (HD EOBD-II N)", + "Reserved", + "Brazil OBD Phase 1 (OBDBr-1)", + "Brazil OBD Phase 2 (OBDBr-2)", + "Korean OBD (KOBD)", + "India OBD I (IOBD I)", + "India OBD II (IOBD II)", + "Heavy Duty Euro OBD Stage VI (HD EOBD-IV)", ] FUEL_TYPES = [ - "Not available" - "Gasoline" - "Methanol" - "Ethanol" - "Diesel" - "LPG" - "CNG" - "Propane" - "Electric" - "Bifuel running Gasoline" - "Bifuel running Methanol" - "Bifuel running Ethanol" - "Bifuel running LPG" - "Bifuel running CNG" - "Bifuel running Propane" - "Bifuel running Electricity" - "Bifuel running electric and combustion engine" - "Hybrid gasoline" - "Hybrid Ethanol" - "Hybrid Diesel" - "Hybrid Electric" - "Hybrid running electric and combustion engine" - "Hybrid Regenerative" - "Bifuel running diesel" + "Not available", + "Gasoline", + "Methanol", + "Ethanol", + "Diesel", + "LPG", + "CNG", + "Propane", + "Electric", + "Bifuel running Gasoline", + "Bifuel running Methanol", + "Bifuel running Ethanol", + "Bifuel running LPG", + "Bifuel running CNG", + "Bifuel running Propane", + "Bifuel running Electricity", + "Bifuel running electric and combustion engine", + "Hybrid gasoline", + "Hybrid Ethanol", + "Hybrid Diesel", + "Hybrid Electric", + "Hybrid running electric and combustion engine", + "Hybrid Regenerative", + "Bifuel running diesel", ] From 63c85f66931d82afeacc5da2d69d186eb5ea6fef Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 23 Oct 2014 22:16:50 -0400 Subject: [PATCH 064/569] readme updates, fixed typo --- README.md | 18 +++++++++++++++--- obd/commands.py | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b4772d7a..d4166d79 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,20 @@ Commands can also be accessed explicitly, either by name, or by code value. The connection = obd.OBD() - if connection.has_command(obd.commands.RPM): # check for existance of sensor - print connection.query(obd.commands.RPM) # get and print value of sensor + + c = obd.commands.RPM + + # OR + + c = obd.commands['RPM'] + + # OR + + c = obd.commands[1][12] # mode 1, PID 12 (decimal) + + + if connection.has_command(c): # check for existance of sensor + print connection.query(c).value # get and print value of sensor Here are a few of the currently supported commands (for a full list, see commands.py): @@ -76,7 +88,7 @@ Here are a few of the currently supported commands (for a full list, see command + Number of warm-ups since codes cleared + Distance traveled since codes cleared + Evaporative system vapor pressure -+ Baromtric Pressure ++ Barometric Pressure + Control module voltage + Relative throttle position + Ambient air temperature diff --git a/obd/commands.py b/obd/commands.py index 5550c1a1..01a163aa 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -151,7 +151,7 @@ def compute(self, _data): OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , "01", "30", 1, count ), OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , "01", "31", 2, distance ), OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , "01", "32", 2, evap_pressure ), - OBDCommand("BAROMETRIC_PRESSURE" , "Baromtric Pressure" , "01", "33", 1, pressure ), + OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , "01", "33", 1, pressure ), OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , "01", "34", 4, current_centered ), OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , "01", "35", 4, current_centered ), OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , "01", "36", 4, current_centered ), From a2255e535abd66cd61c61e6565d52287a73f8974 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 28 Oct 2014 17:08:05 -0400 Subject: [PATCH 065/569] fixed bit decoding error for small numbers --- obd/codes.py | 4 ++-- obd/decoders.py | 12 +++++++----- obd/utils.py | 7 +++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/obd/codes.py b/obd/codes.py index c148050a..e7d46a69 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -2113,9 +2113,9 @@ "EGR and/or VVT System", "PM filter monitoring", "Exhaust Gas Sensor", - None, + "None", "Boost Pressure", - None, + "None", "NOx/SCR Monitor", "NMHC Catalyst", ] diff --git a/obd/decoders.py b/obd/decoders.py index eced6086..739a5561 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -185,7 +185,9 @@ def fuel_rate(_hex): def status(_hex): - bits = bitstring(_hex) + print _hex + bits = bitstring(_hex, 32) + print bits output = {} output["Check Engine Light"] = bitToBool(bits[0]) @@ -198,12 +200,12 @@ def status(_hex): bitToBool(bits[11]))) output["Tests"].append(Test("Fuel System", \ - bitToBool(bits[16]), \ - bitToBool(bits[12]))) + bitToBool(bits[14]), \ + bitToBool(bits[10]))) output["Tests"].append(Test("Components", \ - bitToBool(bits[17]), \ - bitToBool(bits[13]))) + bitToBool(bits[13]), \ + bitToBool(bits[9]))) # different tests for different ignition types diff --git a/obd/utils.py b/obd/utils.py index e5a71875..ea641cc7 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -64,8 +64,11 @@ def unhex(_hex): def unbin(_bin): return int(_bin, 2) -def bitstring(_hex): - return bin(unhex(_hex))[2:] +def bitstring(_hex, bits=None): + b = bin(unhex(_hex))[2:] + if bits is not None: + b = ('0' * (bits - len(b))) + b + return b def bitToBool(_bit): return (_bit == '1') From f0c8740ff6d11acfa3b5f844f532f22f1bee306b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 28 Oct 2014 17:11:29 -0400 Subject: [PATCH 066/569] updated readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d4166d79..aa23f89d 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ Commands can also be accessed explicitly, either by name, or by code value. The Here are a few of the currently supported commands (for a full list, see commands.py): +(note: support for these commands will vary from car to car) + + Calculated Engine Load + Engine Coolant Temperature + Fuel Pressure From 30011af6545b65f209fcd8673e3faa58e711105f Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Oct 2014 23:02:33 -0400 Subject: [PATCH 067/569] fixed status decoder return tuple --- obd/decoders.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 739a5561..d64bf558 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -185,9 +185,7 @@ def fuel_rate(_hex): def status(_hex): - print _hex bits = bitstring(_hex, 32) - print bits output = {} output["Check Engine Light"] = bitToBool(bits[0]) @@ -212,7 +210,7 @@ def status(_hex): if(output["Ignition Type"] == IGNITION_TYPE[0]): # spark for i in range(8): if SPARK_TESTS[i] is not None: - print i + t = Test(SPARK_TESTS[i], \ bitToBool(bits[(2 * 8) + i]), \ bitToBool(bits[(3 * 8) + i])) @@ -229,7 +227,7 @@ def status(_hex): output["Tests"].append(t) - return output + return (output, Unit.NONE) From 081dc13f55e45e8a6bf1cec24acf462a7ea4eb5c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Oct 2014 23:26:14 -0400 Subject: [PATCH 068/569] made debug handlers with printing switch --- obd/__init__.py | 1 + obd/commands.py | 21 +++++++++++++-------- obd/debug.py | 13 +++++++++++++ obd/obd.py | 5 ++++- obd/port.py | 15 ++++++++------- 5 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 obd/debug.py diff --git a/obd/__init__.py b/obd/__init__.py index 56d09095..431cdb02 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -2,3 +2,4 @@ from obd import OBD from commands import commands from utils import scanSerial +from debug import debug diff --git a/obd/commands.py b/obd/commands.py index 01a163aa..e18776ba 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -48,8 +48,7 @@ def clone(self): self.mode, self.pid, self.bytes, - self.decode, - self.supported) + self.decode) def get_command(self): return self.mode + self.pid # the actual command transmitted to the port @@ -257,6 +256,8 @@ def __getitem__(self, key): return self.modes[key] elif isinstance(key, basestring): return self.__dict__[key] + else: + print "OBD commands can only be retrieved by PID value or dict name" def __len__(self): l = 0 @@ -283,13 +284,17 @@ def set_supported(self, mode, pid, v): # checks for existance of int mode and int pid def has(self, mode, pid): - if (mode < 0) or (pid < 0): - return False - if mode >= len(self.modes): - return False - if pid >= len(self.modes[mode]): + if isinstance(mode, int) and isinstance(pid, int): + if (mode < 0) or (pid < 0): + return False + if mode >= len(self.modes): + return False + if pid >= len(self.modes[mode]): + return False + return True + else: + print "has() only accepts integer values for mode and PID" return False - return True # export this object commands = Commands() diff --git a/obd/debug.py b/obd/debug.py new file mode 100644 index 00000000..37152c75 --- /dev/null +++ b/obd/debug.py @@ -0,0 +1,13 @@ + +class Debug(): + def __init__(self): + self.console = False + self.handler = None + + def __call__(self, msg): + if self.console: + print msg + if hasattr(self.handler, '__call__'): + self.handler(msg) + +debug = Debug() \ No newline at end of file diff --git a/obd/obd.py b/obd/obd.py index cf553060..068a0789 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -77,7 +77,10 @@ def load_commands(self): if commands.has(mode, pid): c = commands[mode][pid] c.supported = True - self.supportedCommands.append(c) + + # don't add PID getters to the command list + if c not in pid_getters: + self.supportedCommands.append(c) def print_commands(self): diff --git a/obd/port.py b/obd/port.py index 1a557b03..04ac880c 100644 --- a/obd/port.py +++ b/obd/port.py @@ -26,6 +26,7 @@ import string import time from utils import Response, unhex +from debug import debug class State(): @@ -50,7 +51,7 @@ def __init__(self, portname): self.state = State.Connected self.port = None - print "Opening interface (serial port)" + debug("Opening serial port...") try: self.port = serial.Serial(portname, \ @@ -67,7 +68,7 @@ def __init__(self, portname): self.error(e) return - print "Interface successfully opened on " + self.get_port_name() + debug("Serial port successfully opened on " + self.get_port_name()) try: self.send("atz") # initialize @@ -78,16 +79,15 @@ def __init__(self, portname): self.error("ELMver did not return") return - print "atz response: " + self.ELMver + debug("atz response: " + self.ELMver) except serial.SerialException as e: self.error(e) return self.send("ate0") # echo off - print "ate0 response: " + self.get() - - print "Connected to ECU" + debug("ate0 response: " + self.get()) + debug("Connected to ECU") def error(self, msg=None): @@ -146,7 +146,8 @@ def get(self): if(attempts <= 0): break - print "Got nothing\n" + debug("get() found nothing") + attempts -= 1 continue From f4dff41f4665563721481f612fb2cf8fc39a6e3f Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Oct 2014 23:38:26 -0400 Subject: [PATCH 069/569] added more debug statements --- obd/commands.py | 6 +++++- obd/debug.py | 2 +- obd/obd.py | 6 ++++++ obd/utils.py | 7 ++++--- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index e18776ba..0a82b4ba 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -26,6 +26,7 @@ from decoders import * from utils import * +from debug import debug @@ -65,6 +66,7 @@ def compute(self, _data): # create the response object with the raw data recieved r = Response(_data) + debug("command returned: %s" % _data) # strips spaces, and removes [\n\r\t] _data = "".join(_data.split()) @@ -80,8 +82,10 @@ def compute(self, _data): # decoded value into the response object r.set(self.decode(_data)) + else: - pass # not a parseable response + # not a parseable response + debug("return data could not be decoded") return r diff --git a/obd/debug.py b/obd/debug.py index 37152c75..5e148071 100644 --- a/obd/debug.py +++ b/obd/debug.py @@ -10,4 +10,4 @@ def __call__(self, msg): if hasattr(self.handler, '__call__'): self.handler(msg) -debug = Debug() \ No newline at end of file +debug = Debug() diff --git a/obd/obd.py b/obd/obd.py index 068a0789..2187bb36 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -5,6 +5,7 @@ from port import OBDPort, State from commands import commands from utils import scanSerial, Response +from debug import debug @@ -50,6 +51,8 @@ def get_port_name(self): def load_commands(self): """ queries for available PIDs, sets their support status, and compiles a list of command objects """ + debug("querying for supported PIDs (commands)...") + self.supportedCommands = [] pid_getters = commands.pid_getters() @@ -82,6 +85,8 @@ def load_commands(self): if c not in pid_getters: self.supportedCommands.append(c) + debug("finished querying with %d commands supported" % len(self.supportedCommands)) + def print_commands(self): for c in self.supportedCommands: @@ -94,6 +99,7 @@ def query(self, command, force=False): #print "TX: " + str(command) if self.has_command(command) or force: + debug("Sending command: %s" % str(command)) # send command to the port self.port.send(command.get_command()) diff --git a/obd/utils.py b/obd/utils.py index ea641cc7..bd333070 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -1,7 +1,8 @@ + import serial import errno import string - +from debug import debug class Unit: @@ -87,10 +88,10 @@ def constrainHex(_hex, b): diff = (b * 2) - len(_hex) # length discrepency in number of hex digits if diff > 0: - print "Receieved less data than expected, trying to parse anyways..." + debug("Receieved less data than expected, trying to parse anyways...") _hex += ('0' * diff) # pad the right side with zeros elif diff < 0: - print "Receieved more data than expected, trying to parse anyways..." + debug("Receieved more data than expected, trying to parse anyways...") _hex = _hex[:diff] # chop off the right side to fit return _hex From 02036eb524dbaee269c2d3e274c343b4f44e6f4a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Oct 2014 00:49:18 -0400 Subject: [PATCH 070/569] added debug section to debug --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index aa23f89d..3e8c1ba6 100644 --- a/README.md +++ b/README.md @@ -107,4 +107,19 @@ Here are a few of the currently supported commands (for a full list, see command + Engine fuel rate +### Debug + +python-OBD also contains a debug object that can be used to print status messages and errors. Console printing is disabled by default, but can be enabled manually. A custom debug handler can also be set. + + import obd + + obd.debug.console = True + + # AND / OR + + def log(msg): + print msg + + obd.debug.handler = log + Enjoy and drive safe! From 4a5fe909863f9953818fe93232751db794b146fa Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Oct 2014 01:06:04 -0400 Subject: [PATCH 071/569] added more debug --- obd/commands.py | 6 +++--- obd/debug.py | 6 ++++-- obd/obd.py | 18 +++++------------- obd/port.py | 6 +++--- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 0a82b4ba..55acc7e1 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -261,7 +261,7 @@ def __getitem__(self, key): elif isinstance(key, basestring): return self.__dict__[key] else: - print "OBD commands can only be retrieved by PID value or dict name" + debug("OBD commands can only be retrieved by PID value or dict name", True) def __len__(self): l = 0 @@ -284,7 +284,7 @@ def set_supported(self, mode, pid, v): if (mode < len(self.modes)) and (pid < len(self.modes[mode])): self.modes[mode][pid].supported = v else: - print "set_supported only accepts boolean values" + debug("set_supported only accepts boolean values", True) # checks for existance of int mode and int pid def has(self, mode, pid): @@ -297,7 +297,7 @@ def has(self, mode, pid): return False return True else: - print "has() only accepts integer values for mode and PID" + debug("has() only accepts integer values for mode and PID", True) return False # export this object diff --git a/obd/debug.py b/obd/debug.py index 5e148071..ce922aab 100644 --- a/obd/debug.py +++ b/obd/debug.py @@ -4,9 +4,11 @@ def __init__(self): self.console = False self.handler = None - def __call__(self, msg): - if self.console: + def __call__(self, msg, forcePrint=False): + + if self.console or forcePrint: print msg + if hasattr(self.handler, '__call__'): self.handler(msg) diff --git a/obd/obd.py b/obd/obd.py index 2187bb36..c0045298 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -17,14 +17,18 @@ def __init__(self, portstr=None): self.supportedCommands = [] # initialize by connecting and loading sensors + debug("Starting python-OBD") if self.connect(portstr): self.load_commands() + else: + debug("Failed to connect") def connect(self, portstr=None): """ attempts to instantiate an OBDPort object. Return boolean for success/failure""" if portstr is None: + debug("Using scanSerial to select port") portnames = scanSerial() for port in portnames: @@ -35,6 +39,7 @@ def connect(self, portstr=None): # success! stop searching for serial break else: + debug("Explicit port defined") self.port = OBDPort(portstr) return self.is_connected() @@ -110,16 +115,3 @@ def query(self, command, force=False): else: print "'%s' is not supported" % str(command) return Response() # return empty response - - - - -if __name__ == "__main__": - - o = OBD() - time.sleep(3) - if not o.is_connected(): - print "Not connected" - else: - print "Connected to " + o.get_port_name() - o.print_commands() diff --git a/obd/port.py b/obd/port.py index 04ac880c..2bb04f47 100644 --- a/obd/port.py +++ b/obd/port.py @@ -92,10 +92,10 @@ def __init__(self, portname): def error(self, msg=None): """ called when connection error has been encountered """ - print "Connection Error:" + debug("Connection Error:", True) if msg is not None: - print msg + debug(msg, True) if self.port is not None: self.port.close() @@ -161,7 +161,7 @@ def get(self): else: # whatever is left must be part of the response result = result + c else: - print "NO self.port!" + debug("NO self.port!", True) return result From 5ab751075667c8cd1ac27101280773333425d43d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Oct 2014 13:10:36 -0400 Subject: [PATCH 072/569] added GPL header to __init__ --- obd/__init__.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/obd/__init__.py b/obd/__init__.py index 431cdb02..9daaa996 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -1,4 +1,26 @@ +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# Copyright 2014 Brendan Whitfield # +# # +######################################################################## +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## + from obd import OBD from commands import commands from utils import scanSerial From fe5354f8cbb85b0f744e3ad6fc6c71e8a73b9ce1 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Oct 2014 13:58:32 -0400 Subject: [PATCH 073/569] adding simple cli --- obd/__main__.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 obd/__main__.py diff --git a/obd/__main__.py b/obd/__main__.py new file mode 100644 index 00000000..4bdc7603 --- /dev/null +++ b/obd/__main__.py @@ -0,0 +1,51 @@ + +from sys import stdin, stdout, argv + +stdout.write("=============================\n") +stdout.write("Welcome to the python-OBD CLI\n") +stdout.write("=============================\n") + +# catch the help flag +if ("help" in argv) or ("--help" in argv): + stdout.write(""" +Built for python 2.7 + + python obd + python obd + + OBD >>> + OBD >>> : + +""") + exit() + + + +import obd +obd.debug.console = True + +o = None + +if len(argv) == 1: + o = obd.OBD() # connect using scanSerial +elif len(argv) == 2: + o = obd.OBD(argv[1]) # connect to specified port +else: + stdout.write("Unknown command arguments. Please see 'python obd --help'") + +if o is not None and o.is_connected(): + + stdout.write("Supported Commands\n") + for command in self.supportedCommands: + stdout.write("\t%s" % str(c)) + + while True: + stdout.write("\nOBD >>> ") + i = stdin.readline() + i = i.strip() + i = i.upper() + + if i in ["EXIT", "QUIT"]: + stdout.write("Goodbye\n") + break; + From 9cafef53bd40125dcfb81a3d6cf399461a76688c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Oct 2014 14:40:40 -0400 Subject: [PATCH 074/569] finished simple CLI --- obd/__main__.py | 62 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/obd/__main__.py b/obd/__main__.py index 4bdc7603..fdf02322 100644 --- a/obd/__main__.py +++ b/obd/__main__.py @@ -5,27 +5,31 @@ stdout.write("Welcome to the python-OBD CLI\n") stdout.write("=============================\n") -# catch the help flag -if ("help" in argv) or ("--help" in argv): +def printHelp(): stdout.write(""" Built for python 2.7 - python obd - python obd + python obd auto-detects serial port + python obd opens specified serial port - OBD >>> - OBD >>> : + OBD >>> sends command and prints result + OBD >>> : sends command and prints result + OBD >>> list lists all available commands """) - exit() + +# catch the help flag +if ("help" in argv) or ("--help" in argv): + printHelp() + exit() import obd obd.debug.console = True - o = None +# connect if len(argv) == 1: o = obd.OBD() # connect using scanSerial elif len(argv) == 2: @@ -33,11 +37,17 @@ else: stdout.write("Unknown command arguments. Please see 'python obd --help'") + +def listCommands(o): + stdout.write("Supported Commands\n") + for c in o.supportedCommands: + stdout.write("\t%s:%s\t%s\n" % (c.mode, c.pid, c.desc)) + + + if o is not None and o.is_connected(): - stdout.write("Supported Commands\n") - for command in self.supportedCommands: - stdout.write("\t%s" % str(c)) + listCommands(o) while True: stdout.write("\nOBD >>> ") @@ -49,3 +59,33 @@ stdout.write("Goodbye\n") break; + if i == "HELP": + printHelp() + continue + + if i == "LIST": + listCommands(o) + continue; + + parts = i.split(':') + + try: + # parse the user's input + command = None + + if len(parts) == 1: + command = obd.commands[parts[0]] + elif len(parts) == 2: + mode = int(parts[0], 16) + pid = int(parts[1], 16) + command = obd.commands[mode][pid] + + # send command and print result + if command is not None and o.has_command(command): + r = o.query(command) + stdout.write("\nDecoded Result:\n%s\n" % str(r)) + else: + stdout.write("Unsupported command: %s" % str(command)) + + except: + stdout.write("Could not parse command\n") From 95ddbd9851a30bc357e376dc5d13ba8c5c49667c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Oct 2014 15:04:50 -0400 Subject: [PATCH 075/569] updated GPL headers --- obd/__init__.py | 9 ++++++++- obd/__main__.py | 30 ++++++++++++++++++++++++++++ obd/commands.py | 52 ++++++++++++++++++++++++++----------------------- obd/debug.py | 31 ++++++++++++++++++++++++++++- obd/decoders.py | 29 +++++++++++++++++++++++++++ obd/obd.py | 30 +++++++++++++++++++++++++++- obd/port.py | 52 +++++++++++++++++++++++++++---------------------- obd/utils.py | 29 +++++++++++++++++++++++++++ 8 files changed, 212 insertions(+), 50 deletions(-) diff --git a/obd/__init__.py b/obd/__init__.py index 9daaa996..eefeafaf 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -2,10 +2,17 @@ ######################################################################## # # # python-OBD: A python OBD-II serial module derived from pyobd # -# Copyright 2014 Brendan Whitfield # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # # # ######################################################################## # # +# __init__.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # # python-OBD is free software: you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # # the Free Software Foundation, either version 2 of the License, or # diff --git a/obd/__main__.py b/obd/__main__.py index fdf02322..b79c1a48 100644 --- a/obd/__main__.py +++ b/obd/__main__.py @@ -1,3 +1,33 @@ +#!/usr/bin/env python + +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# # +######################################################################## +# # +# __main__.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## from sys import stdin, stdout, argv diff --git a/obd/commands.py b/obd/commands.py index 55acc7e1..6836b4c0 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -1,28 +1,32 @@ -#!/usr/bin/env python -########################################################################### -# obd_sensors.py -# -# Copyright 2004 Donour Sizemore (donour@uchicago.edu) -# Copyright 2009 Secons Ltd. (www.obdtester.com) -# -# This file is part of pyOBD. -# -# pyOBD is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# pyOBD is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with pyOBD; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -########################################################################### - +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# # +######################################################################## +# # +# commands.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## from decoders import * from utils import * diff --git a/obd/debug.py b/obd/debug.py index ce922aab..eeb0de90 100644 --- a/obd/debug.py +++ b/obd/debug.py @@ -1,11 +1,40 @@ +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# # +######################################################################## +# # +# debug.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## + class Debug(): def __init__(self): self.console = False self.handler = None def __call__(self, msg, forcePrint=False): - + if self.console or forcePrint: print msg diff --git a/obd/decoders.py b/obd/decoders.py index d64bf558..b071edbb 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -1,4 +1,33 @@ +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# # +######################################################################## +# # +# decoders.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## + import math from utils import * from codes import * diff --git a/obd/obd.py b/obd/obd.py index c0045298..4a80666b 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -1,4 +1,32 @@ -#!/usr/bin/env python + +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# # +######################################################################## +# # +# obd.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## import time diff --git a/obd/port.py b/obd/port.py index 2bb04f47..a4809d7b 100644 --- a/obd/port.py +++ b/obd/port.py @@ -1,26 +1,32 @@ - #!/usr/bin/env python -########################################################################### -# odb_io.py -# -# Copyright 2004 Donour Sizemore (donour@uchicago.edu) -# Copyright 2009 Secons Ltd. (www.obdtester.com) -# -# This file is part of pyOBD. -# -# pyOBD is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# pyOBD is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with pyOBD; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -########################################################################### + +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# # +######################################################################## +# # +# port.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## import serial import string diff --git a/obd/utils.py b/obd/utils.py index bd333070..282ae437 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -1,4 +1,33 @@ +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# # +######################################################################## +# # +# utils.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## + import serial import errno import string From 8a08e0d4d536b8b45e9debf94f304b6f9fdc7aac Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Oct 2014 15:08:43 -0400 Subject: [PATCH 076/569] missed one, updated GPL header in codes.py --- obd/codes.py | 52 +++++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/obd/codes.py b/obd/codes.py index e7d46a69..821d1493 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -1,26 +1,32 @@ - #!/usr/bin/env python -########################################################################### -# obd_sensors.py -# -# Copyright 2004 Donour Sizemore (donour@uchicago.edu) -# Copyright 2009 Secons Ltd. (www.obdtester.com) -# -# This file is part of pyOBD. -# -# pyOBD is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# pyOBD is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with pyOBD; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -########################################################################### + +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# # +######################################################################## +# # +# __init__.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## DTC = { "P0001": "Fuel Volume Regulator Control Circuit/Open", From f8fb152e748ea4c7784b0dff548b28abbcc02661 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 3 Nov 2014 22:01:01 -0500 Subject: [PATCH 077/569] changed readme to rst --- README.md => README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename README.md => README.rst (99%) diff --git a/README.md b/README.rst similarity index 99% rename from README.md rename to README.rst index 3e8c1ba6..fecbb268 100644 --- a/README.md +++ b/README.rst @@ -1,5 +1,5 @@ python-OBD -======== +========== ##### A python module for handling realtime sensor data from OBD-II vehicle ports From 0e83bbe2fced39a5b0e15a77b6b02e770f751f66 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 3 Nov 2014 22:06:18 -0500 Subject: [PATCH 078/569] tweaking readme --- README.rst | 71 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/README.rst b/README.rst index fecbb268..c25884ec 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ python-OBD ========== -##### A python module for handling realtime sensor data from OBD-II vehicle ports +A python module for handling realtime sensor data from OBD-II vehicle ports This library is forked from: @@ -9,63 +9,65 @@ This library is forked from: + https://github.com/Pbartek/pyobd-pi -### Dependencies +Dependencies +------------ + pySerial + OBD-II addapter (ELM327 Bluetooth Adapter or ELM327 USB Cable) -### Usage +Usage +----- After installing the library, simply import pyobd, and create a new OBD connection object. By default, python-OBD will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the scanSerial helper retrieve a list of connected ports - import obd + import obd - connection = obd.OBD() # create connection object + connection = obd.OBD() # create connection object - # OR + # OR - connection = obd.OBD("/dev/ttyUSB0") # create connection with USB 0 + connection = obd.OBD("/dev/ttyUSB0") # create connection with USB 0 - # OR + # OR - ports = obd.scanSerial() # return list of valid USB or RF ports - print ports - connection = obd.OBD(ports[0]) # connect to the first port in the list + ports = obd.scanSerial() # return list of valid USB or RF ports + print ports + connection = obd.OBD(ports[0]) # connect to the first port in the list Once a connection is made, python-OBD will load a list of the available commands in your car. A "Command" in python-OBD is an object used to query specific information from the vehicle. A command object contains its name, units, codes, and decoding functions. To get the value of a sensor, call the query() function with that sensor's command as an argument. - import obd + import obd - connection = obd.OBD() - - for command in connection.supportedCommands: - print str(command) # prints the command name - response = connection.query(command) # sends the command, and returns the decoded response - print response.value, response.unit # prints the data and units returned from the car + connection = obd.OBD() + + for command in connection.supportedCommands: + print str(command) # prints the command name + response = connection.query(command) # sends the command, and returns the decoded response + print response.value, response.unit # prints the data and units returned from the car Commands can also be accessed explicitly, either by name, or by code value. The has_command() function will determine whether or not your car supports the requested command. - import obd + import obd - connection = obd.OBD() + connection = obd.OBD() - c = obd.commands.RPM + c = obd.commands.RPM - # OR + # OR - c = obd.commands['RPM'] + c = obd.commands['RPM'] - # OR + # OR - c = obd.commands[1][12] # mode 1, PID 12 (decimal) + c = obd.commands[1][12] # mode 1, PID 12 (decimal) - if connection.has_command(c): # check for existance of sensor - print connection.query(c).value # get and print value of sensor + if connection.has_command(c): # check for existance of sensor + print connection.query(c).value # get and print value of sensor Here are a few of the currently supported commands (for a full list, see commands.py): @@ -107,19 +109,20 @@ Here are a few of the currently supported commands (for a full list, see command + Engine fuel rate -### Debug +Debug +----- python-OBD also contains a debug object that can be used to print status messages and errors. Console printing is disabled by default, but can be enabled manually. A custom debug handler can also be set. - import obd + import obd - obd.debug.console = True + obd.debug.console = True - # AND / OR + # AND / OR - def log(msg): - print msg + def log(msg): + print msg - obd.debug.handler = log + obd.debug.handler = log Enjoy and drive safe! From f4dd1c2ae783a467f915b03d100f9c8dd49b281b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 3 Nov 2014 22:08:03 -0500 Subject: [PATCH 079/569] tweaking readme --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index c25884ec..c45e4f30 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ Dependencies Usage ----- -After installing the library, simply import pyobd, and create a new OBD connection object. By default, python-OBD will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the scanSerial helper retrieve a list of connected ports +After installing the library, simply import pyobd, and create a new OBD connection object. By default, python-OBD will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the scanSerial helper retrieve a list of connected ports:: import obd @@ -36,7 +36,7 @@ After installing the library, simply import pyobd, and create a new OBD connecti connection = obd.OBD(ports[0]) # connect to the first port in the list -Once a connection is made, python-OBD will load a list of the available commands in your car. A "Command" in python-OBD is an object used to query specific information from the vehicle. A command object contains its name, units, codes, and decoding functions. To get the value of a sensor, call the query() function with that sensor's command as an argument. +Once a connection is made, python-OBD will load a list of the available commands in your car. A "Command" in python-OBD is an object used to query specific information from the vehicle. A command object contains its name, units, codes, and decoding functions. To get the value of a sensor, call the query() function with that sensor's command as an argument.:: import obd @@ -48,7 +48,7 @@ Once a connection is made, python-OBD will load a list of the available commands print response.value, response.unit # prints the data and units returned from the car -Commands can also be accessed explicitly, either by name, or by code value. The has_command() function will determine whether or not your car supports the requested command. +Commands can also be accessed explicitly, either by name, or by code value. The has_command() function will determine whether or not your car supports the requested command.:: import obd @@ -112,7 +112,7 @@ Here are a few of the currently supported commands (for a full list, see command Debug ----- -python-OBD also contains a debug object that can be used to print status messages and errors. Console printing is disabled by default, but can be enabled manually. A custom debug handler can also be set. +python-OBD also contains a debug object that can be used to print status messages and errors. Console printing is disabled by default, but can be enabled manually. A custom debug handler can also be set.:: import obd From 1fc1642a074798ccde53cfb29adf9335f1564895 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 3 Nov 2014 22:09:10 -0500 Subject: [PATCH 080/569] tweaking readme --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index c45e4f30..c3df5216 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ Dependencies Usage ----- -After installing the library, simply import pyobd, and create a new OBD connection object. By default, python-OBD will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the scanSerial helper retrieve a list of connected ports:: +After installing the library, simply import pyobd, and create a new OBD connection object. By default, python-OBD will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the scanSerial helper retrieve a list of connected ports: import obd @@ -36,7 +36,7 @@ After installing the library, simply import pyobd, and create a new OBD connecti connection = obd.OBD(ports[0]) # connect to the first port in the list -Once a connection is made, python-OBD will load a list of the available commands in your car. A "Command" in python-OBD is an object used to query specific information from the vehicle. A command object contains its name, units, codes, and decoding functions. To get the value of a sensor, call the query() function with that sensor's command as an argument.:: +Once a connection is made, python-OBD will load a list of the available commands in your car. A "Command" in python-OBD is an object used to query specific information from the vehicle. A command object contains its name, units, codes, and decoding functions. To get the value of a sensor, call the query() function with that sensor's command as an argument.: import obd @@ -48,7 +48,7 @@ Once a connection is made, python-OBD will load a list of the available commands print response.value, response.unit # prints the data and units returned from the car -Commands can also be accessed explicitly, either by name, or by code value. The has_command() function will determine whether or not your car supports the requested command.:: +Commands can also be accessed explicitly, either by name, or by code value. The has_command() function will determine whether or not your car supports the requested command.: import obd @@ -112,7 +112,7 @@ Here are a few of the currently supported commands (for a full list, see command Debug ----- -python-OBD also contains a debug object that can be used to print status messages and errors. Console printing is disabled by default, but can be enabled manually. A custom debug handler can also be set.:: +python-OBD also contains a debug object that can be used to print status messages and errors. Console printing is disabled by default, but can be enabled manually. A custom debug handler can also be set.: import obd From c03ce4cb1e8d859ee508b0d2050cb366a3772e55 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 3 Nov 2014 22:10:06 -0500 Subject: [PATCH 081/569] tweaking readme --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index c3df5216..8c2f0ebb 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ Dependencies Usage ----- -After installing the library, simply import pyobd, and create a new OBD connection object. By default, python-OBD will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the scanSerial helper retrieve a list of connected ports: +After installing the library, simply import pyobd, and create a new OBD connection object. By default, python-OBD will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the scanSerial helper retrieve a list of connected ports:: import obd @@ -36,7 +36,7 @@ After installing the library, simply import pyobd, and create a new OBD connecti connection = obd.OBD(ports[0]) # connect to the first port in the list -Once a connection is made, python-OBD will load a list of the available commands in your car. A "Command" in python-OBD is an object used to query specific information from the vehicle. A command object contains its name, units, codes, and decoding functions. To get the value of a sensor, call the query() function with that sensor's command as an argument.: +Once a connection is made, python-OBD will load a list of the available commands in your car. A "Command" in python-OBD is an object used to query specific information from the vehicle. A command object contains its name, units, codes, and decoding functions. To get the value of a sensor, call the query() function with that sensor's command as an argument:: import obd @@ -48,7 +48,7 @@ Once a connection is made, python-OBD will load a list of the available commands print response.value, response.unit # prints the data and units returned from the car -Commands can also be accessed explicitly, either by name, or by code value. The has_command() function will determine whether or not your car supports the requested command.: +Commands can also be accessed explicitly, either by name, or by code value. The has_command() function will determine whether or not your car supports the requested command:: import obd @@ -112,7 +112,7 @@ Here are a few of the currently supported commands (for a full list, see command Debug ----- -python-OBD also contains a debug object that can be used to print status messages and errors. Console printing is disabled by default, but can be enabled manually. A custom debug handler can also be set.: +python-OBD also contains a debug object that can be used to print status messages and errors. Console printing is disabled by default, but can be enabled manually. A custom debug handler can also be set:: import obd From 4059fc9ca15cbe3b4dda34ea16c0ae981a940c41 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 3 Nov 2014 22:56:35 -0500 Subject: [PATCH 082/569] made setup.py --- README.rst | 2 +- setup.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 setup.py diff --git a/README.rst b/README.rst index 8c2f0ebb..2b1e179c 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ Dependencies Usage ----- -After installing the library, simply import pyobd, and create a new OBD connection object. By default, python-OBD will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the scanSerial helper retrieve a list of connected ports:: +After installing the library, simply import 'obd', and create a new OBD connection object. By default, python-OBD will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the scanSerial helper retrieve a list of connected ports:: import obd diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..9f4751bd --- /dev/null +++ b/setup.py @@ -0,0 +1,28 @@ +#!/bin/env python +# -*- coding: utf8 -*- + +from setuptools import setup, find_packages + +setup( + name="obd", + version="0.1.0", + description=("Serial module for handling live sensor data from a vehicle's OBD-II port"), + classifiers=[ + "Operating System :: POSIX :: Linux", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", + "Topic :: System :: Monitoring", + "Programming Language :: Python :: 2", + "Development Status :: 3 - Alpha", + "Topic :: System :: Logging", + "Intended Audience :: Developers", + ], + keywords="obd obd-II obd-ii obd2 car serial vehicle diagnostic", + author="Brendan Whitfield", + author_email="brendanw@windworksdesign.com", + url="http://github.com/brendanwhitfield/python-OBD", + license="GNU GPLv2", + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=["pyserial"], +) \ No newline at end of file From 1e7f739b95b1bb34b862baa949798bdb0070c6f8 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 4 Nov 2014 14:20:49 -0500 Subject: [PATCH 083/569] used spaces instead of tabs to preserve cleanliness --- README.rst | 2 +- obd/commands.py | 202 ++++++++++++++++++++++++------------------------ 2 files changed, 102 insertions(+), 102 deletions(-) diff --git a/README.rst b/README.rst index 2b1e179c..d148072e 100644 --- a/README.rst +++ b/README.rst @@ -13,7 +13,7 @@ Dependencies ------------ + pySerial -+ OBD-II addapter (ELM327 Bluetooth Adapter or ELM327 USB Cable) ++ OBD-II adapter (ELM327 Bluetooth Adapter or ELM327 USB Cable) Usage diff --git a/obd/commands.py b/obd/commands.py index 6836b4c0..bfcb159b 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -104,107 +104,107 @@ def compute(self, _data): # NOTE: commands MUST be in PID order, one command per PID (for fast lookup using __mode1__[pid]) __mode1__ = [ - # sensor name description mode cmd bytes decoder - OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "01", "00", 4, pid , True), # the first PID getter is assumed to be supported - OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01", 4, status ), - OBDCommand("FREEZE_DTC" , "Freeze DTC" , "01", "02", 2, noop ), - OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03", 2, fuel_status ), - OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , "01", "04", 1, percent ), - OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05", 1, temp ), - OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06", 1, percent_centered ), - OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07", 1, percent_centered ), - OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08", 1, percent_centered ), - OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09", 1, percent_centered ), - OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A", 1, fuel_pressure ), - OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "01", "0B", 1, pressure ), - OBDCommand("RPM" , "Engine RPM" , "01", "0C", 2, rpm ), - OBDCommand("SPEED" , "Vehicle Speed" , "01", "0D", 1, speed ), - OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E", 1, timing_advance ), - OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F", 1, temp ), - OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10", 2, maf ), - OBDCommand("THROTTLE_POS" , "Throttle Position" , "01", "11", 1, percent ), - OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12", 1, air_status ), - OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13", 1, noop ), - OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , "01", "14", 2, sensor_voltage ), - OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , "01", "15", 2, sensor_voltage ), - OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , "01", "16", 2, sensor_voltage ), - OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , "01", "17", 2, sensor_voltage ), - OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , "01", "18", 2, sensor_voltage ), - OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , "01", "19", 2, sensor_voltage ), - OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , "01", "1A", 2, sensor_voltage ), - OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , "01", "1B", 2, sensor_voltage ), - OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "01", "1C", 1, obd_compliance ), - OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D", 1, noop ), - OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E", 1, noop ), - OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F", 2, seconds ), - - # sensor name description mode cmd bytes decoder - OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "01", "20", 4, pid ), - OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , "01", "21", 2, distance ), - OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , "01", "22", 2, fuel_pres_vac ), - OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , "01", "23", 2, fuel_pres_direct ), - OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , "01", "24", 4, sensor_voltage_big ), - OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , "01", "25", 4, sensor_voltage_big ), - OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , "01", "26", 4, sensor_voltage_big ), - OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , "01", "27", 4, sensor_voltage_big ), - OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , "01", "28", 4, sensor_voltage_big ), - OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , "01", "29", 4, sensor_voltage_big ), - OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , "01", "2A", 4, sensor_voltage_big ), - OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , "01", "2B", 4, sensor_voltage_big ), - OBDCommand("COMMANDED_EGR" , "Commanded EGR" , "01", "2C", 1, percent ), - OBDCommand("EGR_ERROR" , "EGR Error" , "01", "2D", 1, percent_centered ), - OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , "01", "2E", 1, percent ), - OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , "01", "2F", 1, percent ), - OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , "01", "30", 1, count ), - OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , "01", "31", 2, distance ), - OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , "01", "32", 2, evap_pressure ), - OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , "01", "33", 1, pressure ), - OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , "01", "34", 4, current_centered ), - OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , "01", "35", 4, current_centered ), - OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , "01", "36", 4, current_centered ), - OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , "01", "37", 4, current_centered ), - OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , "01", "38", 4, current_centered ), - OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , "01", "39", 4, current_centered ), - OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , "01", "3A", 4, current_centered ), - OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , "01", "3B", 4, current_centered ), - OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , "01", "3C", 2, catalyst_temp ), - OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , "01", "3D", 2, catalyst_temp ), - OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , "01", "3E", 2, catalyst_temp ), - OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , "01", "3F", 2, catalyst_temp ), - - # sensor name description mode cmd bytes decoder - OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "01", "40", 4, pid ), - OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "01", "41", 4, todo ), - OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "01", "42", 2, todo ), - OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "01", "43", 2, todo ), - OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "01", "44", 2, todo ), - OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , "01", "45", 1, percent ), - OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , "01", "46", 1, temp ), - OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , "01", "47", 1, percent ), - OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , "01", "48", 1, percent ), - OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , "01", "49", 1, percent ), - OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , "01", "4A", 1, percent ), - OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , "01", "4B", 1, percent ), - OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , "01", "4C", 1, percent ), - OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , "01", "4D", 2, minutes ), - OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , "01", "4E", 2, minutes ), - OBDCommand("MAX_VALUES" , "Various Max values" , "01", "4F", 4, noop ), # todo: decode this - OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , "01", "50", 4, max_maf ), - OBDCommand("FUEL_TYPE" , "Fuel Type" , "01", "51", 1, fuel_type ), - OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , "01", "52", 1, percent ), - OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , "01", "53", 2, abs_evap_pressure ), - OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , "01", "54", 2, evap_pressure_alt ), - OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , "01", "55", 2, percent_centered ), # todo: decode seconds value for banks 3 and 4 - OBDCommand("Long_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , "01", "56", 2, percent_centered ), - OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , "01", "57", 2, percent_centered ), - OBDCommand("Long_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , "01", "58", 2, percent_centered ), - OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , "01", "59", 2, fuel_pres_direct ), - OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , "01", "5A", 1, percent ), - OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , "01", "5B", 1, percent ), - OBDCommand("OIL_TEMP" , "Engine oil temperature" , "01", "5C", 1, temp ), - OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , "01", "5D", 2, inject_timing ), - OBDCommand("FUEL_RATE" , "Engine fuel rate" , "01", "5E", 2, fuel_rate ), - OBDCommand("EMISSION_REQ" , "Designed emission requirements" , "01", "5F", 1, noop ), + # sensor name description mode cmd bytes decoder + OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "01", "00", 4, pid , True), # the first PID getter is assumed to be supported + OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01", 4, status ), + OBDCommand("FREEZE_DTC" , "Freeze DTC" , "01", "02", 2, noop ), + OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03", 2, fuel_status ), + OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , "01", "04", 1, percent ), + OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05", 1, temp ), + OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06", 1, percent_centered ), + OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07", 1, percent_centered ), + OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08", 1, percent_centered ), + OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09", 1, percent_centered ), + OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A", 1, fuel_pressure ), + OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "01", "0B", 1, pressure ), + OBDCommand("RPM" , "Engine RPM" , "01", "0C", 2, rpm ), + OBDCommand("SPEED" , "Vehicle Speed" , "01", "0D", 1, speed ), + OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E", 1, timing_advance ), + OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F", 1, temp ), + OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10", 2, maf ), + OBDCommand("THROTTLE_POS" , "Throttle Position" , "01", "11", 1, percent ), + OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12", 1, air_status ), + OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13", 1, noop ), + OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , "01", "14", 2, sensor_voltage ), + OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , "01", "15", 2, sensor_voltage ), + OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , "01", "16", 2, sensor_voltage ), + OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , "01", "17", 2, sensor_voltage ), + OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , "01", "18", 2, sensor_voltage ), + OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , "01", "19", 2, sensor_voltage ), + OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , "01", "1A", 2, sensor_voltage ), + OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , "01", "1B", 2, sensor_voltage ), + OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "01", "1C", 1, obd_compliance ), + OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D", 1, noop ), + OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E", 1, noop ), + OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F", 2, seconds ), + + # sensor name description mode cmd bytes decoder + OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "01", "20", 4, pid ), + OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , "01", "21", 2, distance ), + OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , "01", "22", 2, fuel_pres_vac ), + OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , "01", "23", 2, fuel_pres_direct ), + OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , "01", "24", 4, sensor_voltage_big ), + OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , "01", "25", 4, sensor_voltage_big ), + OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , "01", "26", 4, sensor_voltage_big ), + OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , "01", "27", 4, sensor_voltage_big ), + OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , "01", "28", 4, sensor_voltage_big ), + OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , "01", "29", 4, sensor_voltage_big ), + OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , "01", "2A", 4, sensor_voltage_big ), + OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , "01", "2B", 4, sensor_voltage_big ), + OBDCommand("COMMANDED_EGR" , "Commanded EGR" , "01", "2C", 1, percent ), + OBDCommand("EGR_ERROR" , "EGR Error" , "01", "2D", 1, percent_centered ), + OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , "01", "2E", 1, percent ), + OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , "01", "2F", 1, percent ), + OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , "01", "30", 1, count ), + OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , "01", "31", 2, distance ), + OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , "01", "32", 2, evap_pressure ), + OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , "01", "33", 1, pressure ), + OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , "01", "34", 4, current_centered ), + OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , "01", "35", 4, current_centered ), + OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , "01", "36", 4, current_centered ), + OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , "01", "37", 4, current_centered ), + OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , "01", "38", 4, current_centered ), + OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , "01", "39", 4, current_centered ), + OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , "01", "3A", 4, current_centered ), + OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , "01", "3B", 4, current_centered ), + OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , "01", "3C", 2, catalyst_temp ), + OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , "01", "3D", 2, catalyst_temp ), + OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , "01", "3E", 2, catalyst_temp ), + OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , "01", "3F", 2, catalyst_temp ), + + # sensor name description mode cmd bytes decoder + OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "01", "40", 4, pid ), + OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "01", "41", 4, todo ), + OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "01", "42", 2, todo ), + OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "01", "43", 2, todo ), + OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "01", "44", 2, todo ), + OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , "01", "45", 1, percent ), + OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , "01", "46", 1, temp ), + OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , "01", "47", 1, percent ), + OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , "01", "48", 1, percent ), + OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , "01", "49", 1, percent ), + OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , "01", "4A", 1, percent ), + OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , "01", "4B", 1, percent ), + OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , "01", "4C", 1, percent ), + OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , "01", "4D", 2, minutes ), + OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , "01", "4E", 2, minutes ), + OBDCommand("MAX_VALUES" , "Various Max values" , "01", "4F", 4, noop ), # todo: decode this + OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , "01", "50", 4, max_maf ), + OBDCommand("FUEL_TYPE" , "Fuel Type" , "01", "51", 1, fuel_type ), + OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , "01", "52", 1, percent ), + OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , "01", "53", 2, abs_evap_pressure ), + OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , "01", "54", 2, evap_pressure_alt ), + OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , "01", "55", 2, percent_centered ), # todo: decode seconds value for banks 3 and 4 + OBDCommand("Long_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , "01", "56", 2, percent_centered ), + OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , "01", "57", 2, percent_centered ), + OBDCommand("Long_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , "01", "58", 2, percent_centered ), + OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , "01", "59", 2, fuel_pres_direct ), + OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , "01", "5A", 1, percent ), + OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , "01", "5B", 1, percent ), + OBDCommand("OIL_TEMP" , "Engine oil temperature" , "01", "5C", 1, temp ), + OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , "01", "5D", 2, inject_timing ), + OBDCommand("FUEL_RATE" , "Engine fuel rate" , "01", "5E", 2, fuel_rate ), + OBDCommand("EMISSION_REQ" , "Designed emission requirements" , "01", "5F", 1, noop ), ] From 4760a2f8fe7dfc4c074d9f45b2024b9a7f76f5e9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 5 Nov 2014 21:47:31 -0500 Subject: [PATCH 084/569] added installation to readme, trimmed the command list --- README.rst | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index d148072e..c3cbbad2 100644 --- a/README.rst +++ b/README.rst @@ -8,6 +8,13 @@ This library is forked from: + https://github.com/peterh/pyobd + https://github.com/Pbartek/pyobd-pi +Installation +------------ + +Run the following command to download/install the latest release from pypi:: + + $ pip install obd + Dependencies ------------ @@ -70,9 +77,7 @@ Commands can also be accessed explicitly, either by name, or by code value. The print connection.query(c).value # get and print value of sensor -Here are a few of the currently supported commands (for a full list, see commands.py): - -(note: support for these commands will vary from car to car) +Here are a few of the currently supported commands (note: support for these commands will vary from car to car): + Calculated Engine Load + Engine Coolant Temperature @@ -85,28 +90,21 @@ Here are a few of the currently supported commands (for a full list, see command + Air Flow Rate (MAF) + Throttle Position + Engine Run Time -+ Distance Traveled with MIL on -+ Fuel Rail Pressure (relative to vacuum) -+ Fuel Rail Pressure (direct inject) + Fuel Level Input + Number of warm-ups since codes cleared + Distance traveled since codes cleared + Evaporative system vapor pressure + Barometric Pressure -+ Control module voltage -+ Relative throttle position + Ambient air temperature + Commanded throttle actuator + Time run with MIL on + Time since trouble codes cleared -+ Fuel Type -+ Ethanol Fuel Percent + Fuel rail pressure (absolute) -+ Relative accelerator pedal position + Hybrid battery pack remaining life + Engine oil temperature + Fuel injection timing + Engine fuel rate ++ etc... (for a full list, see commands.py) Debug From 4d38094390dd59ba537b57e2ed95be6764636a2f Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 5 Nov 2014 21:55:22 -0500 Subject: [PATCH 085/569] trimmed readme even more, added link to commands.py --- README.rst | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.rst b/README.rst index c3cbbad2..620698dc 100644 --- a/README.rst +++ b/README.rst @@ -92,19 +92,14 @@ Here are a few of the currently supported commands (note: support for these comm + Engine Run Time + Fuel Level Input + Number of warm-ups since codes cleared -+ Distance traveled since codes cleared -+ Evaporative system vapor pressure + Barometric Pressure + Ambient air temperature + Commanded throttle actuator + Time run with MIL on + Time since trouble codes cleared -+ Fuel rail pressure (absolute) + Hybrid battery pack remaining life -+ Engine oil temperature -+ Fuel injection timing + Engine fuel rate -+ etc... (for a full list, see commands.py) ++ etc... (for a full list, see `commands.py `_) Debug From 27ff85b541ffcf1880747bf234d9cee70178a908 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 5 Nov 2014 22:01:04 -0500 Subject: [PATCH 086/569] fixed command formatting --- obd/commands.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index bfcb159b..d789b42c 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -219,18 +219,18 @@ def compute(self, _data): __mode3__ = [ - # sensor name description mode cmd bytes decoder - OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 0, noop ), + # sensor name description mode cmd bytes decoder + OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 0, noop ), ] __mode4__ = [ - # sensor name description mode cmd bytes decoder - OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", "" , 0, noop ), + # sensor name description mode cmd bytes decoder + OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", "" , 0, noop ), ] __mode7__ = [ - # sensor name description mode cmd bytes decoder - OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", "" , 0, noop ), + # sensor name description mode cmd bytes decoder + OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", "" , 0, noop ), ] From ed4fce04d7275d438e572f8409e29c8c60275aa9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 6 Nov 2014 14:05:59 -0500 Subject: [PATCH 087/569] minor tweaks, started writing facade DTC getter --- obd/decoders.py | 12 ++++++------ obd/obd.py | 20 ++++++++++++++++++-- obd/port.py | 3 ++- obd/utils.py | 2 +- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index b071edbb..1d67f5d9 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -217,9 +217,9 @@ def status(_hex): bits = bitstring(_hex, 32) output = {} - output["Check Engine Light"] = bitToBool(bits[0]) - output["DTC Count"] = unbin(bits[1:8]) - output["Ignition Type"] = IGNITION_TYPE[unbin(bits[12])] + output["Check_Engine_Light"] = bitToBool(bits[0]) + output["DTC_Count"] = unbin(bits[1:8]) + output["Ignition_Type"] = IGNITION_TYPE[unbin(bits[12])] output["Tests"] = [] output["Tests"].append(Test("Misfire", \ @@ -236,7 +236,7 @@ def status(_hex): # different tests for different ignition types - if(output["Ignition Type"] == IGNITION_TYPE[0]): # spark + if(output["Ignition_Type"] == IGNITION_TYPE[0]): # spark for i in range(8): if SPARK_TESTS[i] is not None: @@ -246,7 +246,7 @@ def status(_hex): output["Tests"].append(t) - elif(output["Ignition Type"] == IGNITION_TYPE[1]): # compression + elif(output["Ignition_Type"] == IGNITION_TYPE[1]): # compression for i in range(8): if COMPRESSION_TESTS[i] is not None: @@ -335,7 +335,7 @@ def dtc(_hex): # converts a frame of 2-byte DTCs into a list of DTCs def dtc_frame(_hex): code_length = 4 # number of hex chars consumed by one code - size = len(_hex / 4) # number of codes defined in THIS FRAME (not total) + size = len(_hex) / code_length # number of codes defined in THIS FRAME (not total) codes = [] for n in range(size): diff --git a/obd/obd.py b/obd/obd.py index 4a80666b..c23898e8 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -129,8 +129,8 @@ def has_command(self, c): return commands.has(c.get_mode_int(), c.get_pid_int()) and c.supported def query(self, command, force=False): - #print "TX: " + str(command) - + """ send the given command, retrieve response, and parse response """ + if self.has_command(command) or force: debug("Sending command: %s" % str(command)) @@ -143,3 +143,19 @@ def query(self, command, force=False): else: print "'%s' is not supported" % str(command) return Response() # return empty response + + def queryDTC(self): + """ read all DTCs """ + + n = self.query(commands.STATUS).value['DTC Count']; + + codes = []; + + # poll until the number of commands received equals that returned from STATUS + # or until this has looped 128 times (the max number of DTCs that STATUS reports) + i = 0 + while (len(codes) < n) and (i < 128): + codes += self.query(commands.GET_DTC).value + i += 1 + + return codes diff --git a/obd/port.py b/obd/port.py index a4809d7b..f309df71 100644 --- a/obd/port.py +++ b/obd/port.py @@ -54,7 +54,7 @@ def __init__(self, portname): timeout = 2 #seconds self.ELMver = "Unknown" - self.state = State.Connected + self.state = State.Unconnected self.port = None debug("Opening serial port...") @@ -94,6 +94,7 @@ def __init__(self, portname): self.send("ate0") # echo off debug("ate0 response: " + self.get()) debug("Connected to ECU") + self.state = State.Connected def error(self, msg=None): diff --git a/obd/utils.py b/obd/utils.py index 282ae437..48b4b9e0 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -112,8 +112,8 @@ def twos_comp(val, num_bits): def isHex(_hex): return all(c in string.hexdigits for c in _hex) -# pads or chops hex to the requested number of bytes def constrainHex(_hex, b): + """pads or chops hex to the requested number of bytes""" diff = (b * 2) - len(_hex) # length discrepency in number of hex digits if diff > 0: From 1baf08d17faf3445a0562c014378117f72634420 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 15 Nov 2014 23:08:01 -0500 Subject: [PATCH 088/569] made quick proof of concept --- obd/__init__.py | 1 + obd/async.py | 27 +++++++++++++++++++++++++++ obd/debug.py | 2 +- obd/port.py | 2 +- 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 obd/async.py diff --git a/obd/__init__.py b/obd/__init__.py index eefeafaf..cfadd802 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -32,3 +32,4 @@ from commands import commands from utils import scanSerial from debug import debug +from async import Async \ No newline at end of file diff --git a/obd/async.py b/obd/async.py new file mode 100644 index 00000000..ff02da99 --- /dev/null +++ b/obd/async.py @@ -0,0 +1,27 @@ + +import obd +import time +import threading + + +class Async(): + """ class representing an OBD-II connection with it's assorted sensors """ + + def __init__(self, portstr=None): + #self.o = obd.OBD(portstr) + self.o = 4 + self.a = 0 + self.sensors = {} + self.thread = threading.Thread(target=self.loop, args=(self.o,)) + self.thread.start() + + + def close(self): + self.thread.join() + + def loop(self, o): + i = 0 + while True: + i+=1 + self.a = i + time.sleep(1) \ No newline at end of file diff --git a/obd/debug.py b/obd/debug.py index eeb0de90..3c8ba9d1 100644 --- a/obd/debug.py +++ b/obd/debug.py @@ -30,7 +30,7 @@ class Debug(): def __init__(self): - self.console = False + self.console = True self.handler = None def __call__(self, msg, forcePrint=False): diff --git a/obd/port.py b/obd/port.py index f309df71..1a9574a5 100644 --- a/obd/port.py +++ b/obd/port.py @@ -140,7 +140,7 @@ def send(self, cmd): def get(self): """Internal use only: not a public interface""" - attempts = 5 + attempts = 2 result = "" if self.port is not None: From 7e51557632f568c38356b021fb73e89053566934 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 15 Nov 2014 23:40:39 -0500 Subject: [PATCH 089/569] drafted a classier thread system --- obd/async.py | 45 ++++++++++++++++++++++++++++++++------------- obd/commands.py | 11 ++++++++--- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/obd/async.py b/obd/async.py index ff02da99..cef88264 100644 --- a/obd/async.py +++ b/obd/async.py @@ -2,26 +2,45 @@ import obd import time import threading +from util import Response + + +class OBDThread(threading.Thread): + def __init__(self, portstr): + super(OBDThread, self).__init__() + + self.connection = obd.OBD(portstr) + self._stop = threading.Event() + self.commands = {} # key = OBDCommand, value = Response + + def stop(self): + self._stop.set() + + def addCommand(self, c): + if not self.commands.has_key(c): + self.commands[c] = Response() # give it an initial value + + def run(self): + # loop until the stop signal is recieved + while not self._stop.isSet(): + if len(self.commands) > 0: + for command in self.commands: + pass + else: + time.sleep(1) class Async(): - """ class representing an OBD-II connection with it's assorted sensors """ + """ class representing an OBD-II connection """ def __init__(self, portstr=None): - #self.o = obd.OBD(portstr) - self.o = 4 - self.a = 0 - self.sensors = {} - self.thread = threading.Thread(target=self.loop, args=(self.o,)) + self.thread = OBDThread(portstr) self.thread.start() - def close(self): + self.thread.stop() self.thread.join() - def loop(self, o): - i = 0 - while True: - i+=1 - self.a = i - time.sleep(1) \ No newline at end of file + def addCommand(self, *commands): + for c in commands: + self.thread.addCommand(c) diff --git a/obd/commands.py b/obd/commands.py index d789b42c..0712930b 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -44,9 +44,6 @@ def __init__(self, name, desc, mode, pid, returnBytes, decoder, supported=False) self.decode = decoder self.supported = supported - def __str__(self): - return "%s%s: %s" % (self.mode, self.pid, self.desc) - def clone(self): return OBDCommand(self.name, self.desc, @@ -93,7 +90,15 @@ def compute(self, _data): return r + def __str__(self): + return "%s%s: %s" % (self.mode, self.pid, self.desc) + + def __hash__(self): + # needed for using commands as keys in a dict (see async.py) + return hash((self.mode, self.pid)) + def __eq__(self, other): + return (self.mode, self.pid) == (other.mode, other.pid) ''' From 8ece6b82015a87521fcf9bb7298b81b0ba13252e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 16 Nov 2014 01:00:07 -0500 Subject: [PATCH 090/569] simplifying async down to one class --- obd/async.py | 63 +++++++++++++++++++++++++++------------------------- obd/obd.py | 9 +++++++- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/obd/async.py b/obd/async.py index cef88264..77c84cc3 100644 --- a/obd/async.py +++ b/obd/async.py @@ -2,45 +2,48 @@ import obd import time import threading -from util import Response +from utils import Response +from commands import OBDCommand -class OBDThread(threading.Thread): - def __init__(self, portstr): - super(OBDThread, self).__init__() +class Async(): + """ class representing an OBD-II connection """ + def __init__(self, portstr=None): self.connection = obd.OBD(portstr) - self._stop = threading.Event() + self.running = True self.commands = {} # key = OBDCommand, value = Response + + if self.connection.is_connected(): + self.thread = threading.Thread(target=self.run, args=(self.connection,)) + self.thread.start() + + def close(self): + self.running = False + self.thread.join() + self.connection.close + + def get(self, c): + if self.commands.has_key(c): + return self.commands[c] + else: + return Response() - def stop(self): - self._stop.set() + def add(self, *commands): + for c in commands: + if isinstance(c, OBDCommand) and self.connection.has_command(c): + if not self.commands.has_key(c): + self.commands[c] = Response() # give it an initial value - def addCommand(self, c): - if not self.commands.has_key(c): - self.commands[c] = Response() # give it an initial value + def remove(self, c): + self.commands.pop(c, None) - def run(self): + def run(self, connection): # loop until the stop signal is recieved - while not self._stop.isSet(): + while self.running: if len(self.commands) > 0: - for command in self.commands: - pass + # loop over the requested commands, and collect the result + for c in self.commands: + self.commands[c] = connection.query(c) else: time.sleep(1) - - -class Async(): - """ class representing an OBD-II connection """ - - def __init__(self, portstr=None): - self.thread = OBDThread(portstr) - self.thread.start() - - def close(self): - self.thread.stop() - self.thread.join() - - def addCommand(self, *commands): - for c in commands: - self.thread.addCommand(c) diff --git a/obd/obd.py b/obd/obd.py index c23898e8..5a849092 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -72,6 +72,13 @@ def connect(self, portstr=None): return self.is_connected() + + def close(self): + if self.is_connected: + self.port.close() + self.port = None + + # checks the port state for conncetion status def is_connected(self): return (self.port is not None) and self.port.is_connected() @@ -144,7 +151,7 @@ def query(self, command, force=False): print "'%s' is not supported" % str(command) return Response() # return empty response - def queryDTC(self): + def query_DTC(self): """ read all DTCs """ n = self.query(commands.STATUS).value['DTC Count']; From 547479b7cb229e212ee6ac9f8c629e830f2a2931 Mon Sep 17 00:00:00 2001 From: David Jones Date: Mon, 17 Nov 2014 02:21:41 +0000 Subject: [PATCH 091/569] Unit.SEC, not Unit.SECONDS --- obd/decoders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/decoders.py b/obd/decoders.py index 1d67f5d9..738d9c9a 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -187,7 +187,7 @@ def max_maf(_hex): # 0 to 65535 seconds def seconds(_hex): v = unhex(_hex) - return (v, Unit.SECONDS) + return (v, Unit.SEC) # 0 to 65535 minutes def minutes(_hex): From 339c2d961489ec8e0c9c734e930f55ef5ccdf981 Mon Sep 17 00:00:00 2001 From: David Jones Date: Wed, 19 Nov 2014 22:42:12 +0000 Subject: [PATCH 092/569] Fixed misplaced closing bracket --- obd/decoders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 738d9c9a..8b1d9888 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -132,8 +132,8 @@ def fuel_pres_direct(_hex): # -8192 to 8192 Pa def evap_pressure(_hex): # decode the twos complement - a = twos_comp(unhex(_hex[0:2], 8)) - b = twos_comp(unhex(_hex[2:4], 8)) + a = twos_comp(unhex(_hex[0:2]) 8) + b = twos_comp(unhex(_hex[2:4]) 8) v = ((a * 256.0) + b) / 4.0 return (v, Unit.PA) From aa9959a4226c06ea56a07c8e7d5b36a8d1af0d90 Mon Sep 17 00:00:00 2001 From: David Jones Date: Sat, 22 Nov 2014 03:22:27 +0000 Subject: [PATCH 093/569] Fixed it properly this time --- obd/decoders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 8b1d9888..bfbfe63a 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -132,8 +132,8 @@ def fuel_pres_direct(_hex): # -8192 to 8192 Pa def evap_pressure(_hex): # decode the twos complement - a = twos_comp(unhex(_hex[0:2]) 8) - b = twos_comp(unhex(_hex[2:4]) 8) + a = twos_comp(unhex(_hex[0:2]), 8) + b = twos_comp(unhex(_hex[2:4]), 8) v = ((a * 256.0) + b) / 4.0 return (v, Unit.PA) From b159e9bd0eaf23786f4c5dda50f6c47fc7431172 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 23 Nov 2014 15:28:45 -0500 Subject: [PATCH 094/569] changed api and fixed start/stop thread creation --- obd/async.py | 32 ++++++++++++++++++++------------ obd/port.py | 2 +- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/obd/async.py b/obd/async.py index 77c84cc3..da648a4e 100644 --- a/obd/async.py +++ b/obd/async.py @@ -11,33 +11,41 @@ class Async(): def __init__(self, portstr=None): self.connection = obd.OBD(portstr) - self.running = True self.commands = {} # key = OBDCommand, value = Response - + self.thread = None + self.start() + + def start(self): + self.running = True if self.connection.is_connected(): self.thread = threading.Thread(target=self.run, args=(self.connection,)) self.thread.start() - def close(self): + def stop(self): self.running = False - self.thread.join() - self.connection.close + if self.thread is not None: + self.thread.join() + self.thread = None - def get(self, c): - if self.commands.has_key(c): - return self.commands[c] - else: - return Response() + def close(self): + self.stop() + self.connection.close() - def add(self, *commands): + def watch(self, *commands): for c in commands: if isinstance(c, OBDCommand) and self.connection.has_command(c): if not self.commands.has_key(c): self.commands[c] = Response() # give it an initial value - def remove(self, c): + def unwatch(self, c): self.commands.pop(c, None) + def get(self, c): + if self.commands.has_key(c): + return self.commands[c] + else: + return Response() + def run(self, connection): # loop until the stop signal is recieved while self.running: diff --git a/obd/port.py b/obd/port.py index 1a9574a5..d7bf0040 100644 --- a/obd/port.py +++ b/obd/port.py @@ -140,7 +140,7 @@ def send(self, cmd): def get(self): """Internal use only: not a public interface""" - attempts = 2 + attempts = 1 result = "" if self.port is not None: From caafc718aaad395344e999d61b623bef80237ab5 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 23 Nov 2014 16:21:29 -0500 Subject: [PATCH 095/569] added is_connected protection --- obd/async.py | 27 +++++++++++++++++++++------ obd/obd.py | 46 +++++++++++++++++++++++++++------------------- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/obd/async.py b/obd/async.py index da648a4e..4f412a05 100644 --- a/obd/async.py +++ b/obd/async.py @@ -13,7 +13,8 @@ def __init__(self, portstr=None): self.connection = obd.OBD(portstr) self.commands = {} # key = OBDCommand, value = Response self.thread = None - self.start() + self.running = False + #self.start() def start(self): self.running = True @@ -32,10 +33,22 @@ def close(self): self.connection.close() def watch(self, *commands): + + errors = [] + for c in commands: - if isinstance(c, OBDCommand) and self.connection.has_command(c): - if not self.commands.has_key(c): - self.commands[c] = Response() # give it an initial value + if not isinstance(c, OBDCommand): + errors.append(c) + continue + + if not self.connection.has_command(c): + errors.append(c) + continue + + if not self.commands.has_key(c): + self.commands[c] = Response() # give it an initial value + + return errors def unwatch(self, c): self.commands.pop(c, None) @@ -49,9 +62,11 @@ def get(self, c): def run(self, connection): # loop until the stop signal is recieved while self.running: + if len(self.commands) > 0: # loop over the requested commands, and collect the result for c in self.commands: + print c self.commands[c] = connection.query(c) - else: - time.sleep(1) + #else: + time.sleep(1) diff --git a/obd/obd.py b/obd/obd.py index 5a849092..b5d75e7a 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -45,15 +45,13 @@ def __init__(self, portstr=None): self.supportedCommands = [] # initialize by connecting and loading sensors - debug("Starting python-OBD") - if self.connect(portstr): - self.load_commands() - else: - debug("Failed to connect") + self.connect(portstr) def connect(self, portstr=None): - """ attempts to instantiate an OBDPort object. Return boolean for success/failure""" + """ attempts to instantiate an OBDPort object. Loads commands on success""" + + debug("Starting python-OBD") if portstr is None: debug("Using scanSerial to select port") @@ -70,11 +68,15 @@ def connect(self, portstr=None): debug("Explicit port defined") self.port = OBDPort(portstr) - return self.is_connected() + # if a connection was made, query for commands + if self.is_connected(): + self.load_commands() + else: + debug("Failed to connect") def close(self): - if self.is_connected: + if self.is_connected(): self.port.close() self.port = None @@ -85,7 +87,10 @@ def is_connected(self): def get_port_name(self): - return self.port.get_port_name() + if self.is_connected(): + return self.port.get_port_name() + else: + return "Not connected to any port" def load_commands(self): @@ -138,19 +143,22 @@ def has_command(self, c): def query(self, command, force=False): """ send the given command, retrieve response, and parse response """ - if self.has_command(command) or force: - debug("Sending command: %s" % str(command)) + # check for a connection + if not self.is_connected(): + debug("Query failed, no connection available", True) + return Response() # return empty response - # send command to the port - self.port.send(command.get_command()) - - # get the data, and compute a response object - return command.compute(self.port.get()) - - else: - print "'%s' is not supported" % str(command) + # check that the command is supported + if not (self.has_command(command) or force): + debug("'%s' is not supported" % str(command), True) return Response() # return empty response + # send the query + debug("Sending command: %s" % str(command)) + self.port.send(command.get_command()) # send command to the port + return command.compute(self.port.get()) # get the data, and compute a response object + + def query_DTC(self): """ read all DTCs """ From a17f38db0b1aab0aa2c0cc062ff8cce5e2a26706 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 23 Nov 2014 17:07:03 -0500 Subject: [PATCH 096/569] misc cleanup --- obd/async.py | 7 +++---- obd/obd.py | 3 ++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/obd/async.py b/obd/async.py index 4f412a05..9e05fcb8 100644 --- a/obd/async.py +++ b/obd/async.py @@ -14,7 +14,7 @@ def __init__(self, portstr=None): self.commands = {} # key = OBDCommand, value = Response self.thread = None self.running = False - #self.start() + self.start() def start(self): self.running = True @@ -66,7 +66,6 @@ def run(self, connection): if len(self.commands) > 0: # loop over the requested commands, and collect the result for c in self.commands: - print c self.commands[c] = connection.query(c) - #else: - time.sleep(1) + else: + time.sleep(1) diff --git a/obd/obd.py b/obd/obd.py index b5d75e7a..ba958ee1 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -158,7 +158,7 @@ def query(self, command, force=False): self.port.send(command.get_command()) # send command to the port return command.compute(self.port.get()) # get the data, and compute a response object - + ''' def query_DTC(self): """ read all DTCs """ @@ -174,3 +174,4 @@ def query_DTC(self): i += 1 return codes + ''' From d898f71b9ff6db21757c132c410974ca446bb1b4 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 25 Nov 2014 16:16:42 -0500 Subject: [PATCH 097/569] added GPL header to async --- obd/async.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/obd/async.py b/obd/async.py index 9e05fcb8..f8b73cc5 100644 --- a/obd/async.py +++ b/obd/async.py @@ -1,4 +1,33 @@ +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# # +######################################################################## +# # +# async.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## + import obd import time import threading From f868bc2f490b0e32f7c3f59a2cd5703d9833992e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 25 Nov 2014 18:20:56 -0500 Subject: [PATCH 098/569] added tests for most decoders --- obd/decoders.py | 2 +- tests/test_decoders.py | 114 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 tests/test_decoders.py diff --git a/obd/decoders.py b/obd/decoders.py index 738d9c9a..bc67d0aa 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -140,7 +140,7 @@ def evap_pressure(_hex): # 0 to 327.675 kPa def abs_evap_pressure(_hex): v = unhex(_hex) - v = v / 200 + v = v / 200.0 return (v, Unit.KPA) # -32767 to 32768 Pa diff --git a/tests/test_decoders.py b/tests/test_decoders.py new file mode 100644 index 00000000..27ff059a --- /dev/null +++ b/tests/test_decoders.py @@ -0,0 +1,114 @@ + +from obd.utils import Unit +import obd.decoders as d + + +def test_count(): + assert d.count("0") == (0, Unit.COUNT) + assert d.count("F") == (15, Unit.COUNT) + assert d.count("3E8") == (1000, Unit.COUNT) + +def test_percent(): + assert d.percent("00") == (0.0, Unit.PERCENT) + assert d.percent("FF") == (100.0, Unit.PERCENT) + +def test_percent_centered(): + assert d.percent_centered("00") == (-100.0, Unit.PERCENT) + assert d.percent_centered("80") == (0.0, Unit.PERCENT) + #assert d.percent_centered("FF") == (100.0, Unit.PERCENT) # returns 99.21875, need float checking or better math + +def test_temp(): + assert d.temp("00") == (-40, Unit.C) + assert d.temp("FF") == (215, Unit.C) + assert d.temp("3E8") == (960, Unit.C) + +def test_catalyst_temp(): + assert d.catalyst_temp("0000") == (-40.0, Unit.C) + assert d.catalyst_temp("FFFF") == (6513.5, Unit.C) + +def test_current_centered(): + assert d.current_centered("00000000") == (-128.0, Unit.MA) + assert d.current_centered("00008000") == (0.0, Unit.MA) + #assert d.current_centered("0000FFFF") == (128.0, Unit.MA) # returns 127.99609375, need float checking or better math + assert d.current_centered("ABCD8000") == (0.0, Unit.MA) # first 2 bytes are unused (should be disregarded) + +def test_sensor_voltage(): + assert d.sensor_voltage("0000") == (0.0, Unit.VOLT) + assert d.sensor_voltage("FFFF") == (1.275, Unit.VOLT) + +def test_sensor_voltage_big(): + assert d.sensor_voltage_big("00000000") == (0.0, Unit.VOLT) + #assert d.sensor_voltage_big("00008000") == (4.0, Unit.VOLT) # returns 127.99609375, need float checking or better math + assert d.sensor_voltage_big("0000FFFF") == (8.0, Unit.VOLT) + assert d.sensor_voltage_big("ABCD0000") == (0.0, Unit.VOLT) # first 2 bytes are unused (should be disregarded) + +def test_fuel_pressure(): + assert d.fuel_pressure("00") == (0, Unit.KPA) + assert d.fuel_pressure("80") == (384, Unit.KPA) + assert d.fuel_pressure("FF") == (765, Unit.KPA) + +def test_pressure(): + assert d.pressure("00") == (0, Unit.KPA) + assert d.pressure("00") == (0, Unit.KPA) + +def test_fuel_pres_vac(): + assert d.fuel_pres_vac("0000") == (0.0, Unit.KPA) + assert d.fuel_pres_vac("FFFF") == (5177.265, Unit.KPA) + +def test_fuel_pres_direct(): + assert d.fuel_pres_direct("0000") == (0, Unit.KPA) + assert d.fuel_pres_direct("FFFF") == (655350, Unit.KPA) + +def test_evap_pressure(): + pass + #assert d.evap_pressure("0000") == (0.0, Unit.PA) + +def test_abs_evap_pressure(): + assert d.abs_evap_pressure("0000") == (0, Unit.KPA) + assert d.abs_evap_pressure("FFFF") == (327, Unit.KPA) + +def test_evap_pressure_alt(): + assert d.evap_pressure_alt("0000") == (-32767, Unit.PA) + assert d.evap_pressure_alt("7FFF") == (0, Unit.PA) + assert d.evap_pressure_alt("FFFF") == (32768, Unit.PA) + +def test_rpm(): + assert d.rpm("0000") == (0.0, Unit.RPM) + assert d.rpm("FFFF") == (16383.75, Unit.RPM) + +def test_speed(): + assert d.speed("00") == (0, Unit.KPH) + assert d.speed("FF") == (255, Unit.KPH) + +def test_timing_advance(): + assert d.timing_advance("00") == (-64.0, Unit.DEGREES) + assert d.timing_advance("FF") == (63.5, Unit.DEGREES) + +def test_inject_timing(): + assert d.inject_timing("0000") == (-210, Unit.DEGREES) + #assert d.inject_timing("FFFF") == (301, Unit.DEGREES) + +def test_maf(): + assert d.maf("0000") == (0.0, Unit.GPS) + assert d.maf("FFFF") == (655.35, Unit.GPS) + +def test_max_maf(): + assert d.max_maf("00000000") == (0, Unit.GPS) + assert d.max_maf("FF000000") == (2550, Unit.GPS) + assert d.max_maf("00ABCDEF") == (0, Unit.GPS) # last 3 bytes are unused (should be disregarded) + +def test_seconds(): + assert d.seconds("0000") == (0, Unit.SEC) + assert d.seconds("FFFF") == (65535, Unit.SEC) + +def test_minutes(): + assert d.minutes("0000") == (0, Unit.MIN) + assert d.minutes("FFFF") == (65535, Unit.MIN) + +def test_distance(): + assert d.distance("0000") == (0, Unit.KM) + assert d.distance("FFFF") == (65535, Unit.KM) + +def test_fuel_rate(): + assert d.fuel_rate("0000") == (0.0, Unit.LPH) + assert d.fuel_rate("FFFF") == (3276.75, Unit.LPH) From c0f0c9bdfd0fc81ae1cd5fd8d37ac397b5976be5 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 25 Nov 2014 22:22:47 -0500 Subject: [PATCH 099/569] wrote initial tests for OBDCommand objects --- obd/commands.py | 2 +- obd/decoders.py | 2 +- obd/obd.py | 2 +- obd/utils.py | 4 ++-- tests/test_OBDCommand.py | 46 ++++++++++++++++++++++++++++++++++++++++ tests/test_decoders.py | 11 ++++++++-- 6 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 tests/test_OBDCommand.py diff --git a/obd/commands.py b/obd/commands.py index d789b42c..c3a1cbef 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -82,7 +82,7 @@ def compute(self, _data): # constrain number of bytes in response if (self.bytes > 0): # zero bytes means flexible response - constrainHex(_data, self.bytes) + _data = constrainHex(_data, self.bytes) # decoded value into the response object r.set(self.decode(_data)) diff --git a/obd/decoders.py b/obd/decoders.py index 055b2b44..ab92563a 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -52,7 +52,7 @@ def noop(_hex): # hex in, bitstring out def pid(_hex): - v = bitstring(_hex) + v = bitstring(_hex, len(_hex) * 4) return (v, Unit.NONE) ''' diff --git a/obd/obd.py b/obd/obd.py index c23898e8..d3b88c61 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -98,7 +98,7 @@ def load_commands(self): response = self.query(get) # ask nicely - if response.isEmpty(): + if response.isNull(): continue supported = response.value # string of binary 01010101010101 diff --git a/obd/utils.py b/obd/utils.py index 48b4b9e0..74f76ee8 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -63,8 +63,8 @@ def __init__(self, raw_data=""): self.unit = Unit.NONE self.raw_data = raw_data - def isEmpty(self): - return (self.value == None) or (len(self.raw_data) == 0) + def isNull(self): + return (self.value == "No Data") or (len(self.raw_data) == 0) def set(self, decode): self.value = decode[0] diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py new file mode 100644 index 00000000..afb38b88 --- /dev/null +++ b/tests/test_OBDCommand.py @@ -0,0 +1,46 @@ + +from obd.commands import OBDCommand +from obd.decoders import noop + + +def test_basic_OBDCommand(): + # name description mode cmd bytes decoder + cmd = OBDCommand("Test", "example OBD command", "01", "23", 2, noop) + assert cmd.name == "Test" + assert cmd.desc == "example OBD command" + assert cmd.mode == "01" + assert cmd.pid == "23" + assert cmd.bytes == 2 + assert cmd.decode == noop + assert cmd.supported == False + + assert cmd.get_command() == "0123" + assert cmd.get_mode_int() == 1 + assert cmd.get_pid_int() == 35 + + cmd = OBDCommand("Test", "example OBD command", "01", "23", 2, noop, True) + assert cmd.supported == True + + +def test_data_stripping(): + # name description mode cmd bytes decoder + cmd = OBDCommand("Test", "example OBD command", "01", "00", 2, noop) + r = cmd.compute("41 00 01 01\r\n") + assert not r.isNull() + assert r.value == "0101" + + +def test_data_not_hex(): + # name description mode cmd bytes decoder + cmd = OBDCommand("Test", "example OBD command", "01", "00", 2, noop) + r = cmd.compute("41 00 wx yz\r\n") + assert r.isNull() + + +def test_data_length(): + # name description mode cmd bytes decoder + cmd = OBDCommand("Test", "example OBD command", "01", "00", 2, noop) + r = cmd.compute("41 00 01 23 45\r\n") + assert r.value == "0123" + r = cmd.compute("41 00 01\r\n") + assert r.value == "0100" diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 27ff059a..11cb1da0 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -3,6 +3,13 @@ import obd.decoders as d +def test_noop(): + assert d.noop("No Operation") == ("No Operation", Unit.NONE) + +def test_pid(): + assert d.pid("00000000") == ("00000000000000000000000000000000", Unit.NONE) + assert d.pid("F00AA00F") == ("11110000000010101010000000001111", Unit.NONE) + def test_count(): assert d.count("0") == (0, Unit.COUNT) assert d.count("F") == (15, Unit.COUNT) @@ -64,8 +71,8 @@ def test_evap_pressure(): #assert d.evap_pressure("0000") == (0.0, Unit.PA) def test_abs_evap_pressure(): - assert d.abs_evap_pressure("0000") == (0, Unit.KPA) - assert d.abs_evap_pressure("FFFF") == (327, Unit.KPA) + assert d.abs_evap_pressure("0000") == (0, Unit.KPA) + assert d.abs_evap_pressure("FFFF") == (327.675, Unit.KPA) def test_evap_pressure_alt(): assert d.evap_pressure_alt("0000") == (-32767, Unit.PA) From 2acdea02c0e9755450976486e7171d3c687f3aee Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 25 Nov 2014 22:30:05 -0500 Subject: [PATCH 100/569] created testing readme --- tests/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/README.md diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..e264e0c7 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,10 @@ +Testing +======= + +To test python-OBD, you will need to `pip install pytest` and install the module (preferably in a virtualenv) by running `python setup.py install` + +To run all tests, run the following command: + + $ py.test tests/ + +For more information on pytest with virtualenvs, [read more here](http://pytest.org/latest/goodpractises.html) \ No newline at end of file From f306be554f8a51528af711dccf84f7b6d15fa347 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 26 Nov 2014 01:51:27 -0500 Subject: [PATCH 101/569] made the readme short and sweet (moved stuff to the wiki) --- README.rst | 79 ++++++++----------------------------------------- obd/__init__.py | 4 +-- 2 files changed, 15 insertions(+), 68 deletions(-) diff --git a/README.rst b/README.rst index 620698dc..c0980a3f 100644 --- a/README.rst +++ b/README.rst @@ -1,12 +1,7 @@ python-OBD ========== -A python module for handling realtime sensor data from OBD-II vehicle ports - -This library is forked from: - -+ https://github.com/peterh/pyobd -+ https://github.com/Pbartek/pyobd-pi +A python module for handling realtime sensor data from OBD-II vehicle ports. Installation ------------ @@ -23,59 +18,23 @@ Dependencies + OBD-II adapter (ELM327 Bluetooth Adapter or ELM327 USB Cable) -Usage ------ - -After installing the library, simply import 'obd', and create a new OBD connection object. By default, python-OBD will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the scanSerial helper retrieve a list of connected ports:: - - import obd - - connection = obd.OBD() # create connection object - - # OR - - connection = obd.OBD("/dev/ttyUSB0") # create connection with USB 0 - - # OR - - ports = obd.scanSerial() # return list of valid USB or RF ports - print ports - connection = obd.OBD(ports[0]) # connect to the first port in the list - - -Once a connection is made, python-OBD will load a list of the available commands in your car. A "Command" in python-OBD is an object used to query specific information from the vehicle. A command object contains its name, units, codes, and decoding functions. To get the value of a sensor, call the query() function with that sensor's command as an argument:: - - import obd - - connection = obd.OBD() - - for command in connection.supportedCommands: - print str(command) # prints the command name - response = connection.query(command) # sends the command, and returns the decoded response - print response.value, response.unit # prints the data and units returned from the car - +Basic Usage +----------- -Commands can also be accessed explicitly, either by name, or by code value. The has_command() function will determine whether or not your car supports the requested command:: +After installing the library, simply import 'obd', and create a new OBD connection object. Commands can then be sent to the car using the `query` function:: import obd - connection = obd.OBD() - - - c = obd.commands.RPM - - # OR + connection = obd.OBD() # auto-connects to USB or RF port - c = obd.commands['RPM'] + cmd = obd.commands.RPM # select an OBD command (sensor) - # OR + response = connection.query(cmd) # send the command, and parse the response - c = obd.commands[1][12] # mode 1, PID 12 (decimal) - - - if connection.has_command(c): # check for existance of sensor - print connection.query(c).value # get and print value of sensor + print(response.value) + print(response.unit) +For more in-depth documentation, `visit the Wiki! ` Here are a few of the currently supported commands (note: support for these commands will vary from car to car): @@ -101,21 +60,9 @@ Here are a few of the currently supported commands (note: support for these comm + Engine fuel rate + etc... (for a full list, see `commands.py `_) +This library is forked from: -Debug ------ - -python-OBD also contains a debug object that can be used to print status messages and errors. Console printing is disabled by default, but can be enabled manually. A custom debug handler can also be set:: - - import obd - - obd.debug.console = True - - # AND / OR - - def log(msg): - print msg - - obd.debug.handler = log ++ https://github.com/peterh/pyobd ++ https://github.com/Pbartek/pyobd-pi Enjoy and drive safe! diff --git a/obd/__init__.py b/obd/__init__.py index eefeafaf..8af0118c 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -29,6 +29,6 @@ ######################################################################## from obd import OBD -from commands import commands -from utils import scanSerial +from commands import commands, OBDCommand +from utils import scanSerial, Unit from debug import debug From 1fe8f35699dd0937bc2c10bb19dd293d0a14a77e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 26 Nov 2014 01:52:41 -0500 Subject: [PATCH 102/569] fixed .rst link --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c0980a3f..08712c53 100644 --- a/README.rst +++ b/README.rst @@ -34,7 +34,7 @@ After installing the library, simply import 'obd', and create a new OBD connecti print(response.value) print(response.unit) -For more in-depth documentation, `visit the Wiki! ` +For more in-depth documentation, `visit the Wiki! `_ Here are a few of the currently supported commands (note: support for these commands will vary from car to car): From 6e4c68763cf3375593871416cdb83f1c7903eea3 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 26 Nov 2014 01:54:37 -0500 Subject: [PATCH 103/569] made docs link more prominent --- README.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 08712c53..f04f8f59 100644 --- a/README.rst +++ b/README.rst @@ -34,7 +34,10 @@ After installing the library, simply import 'obd', and create a new OBD connecti print(response.value) print(response.unit) -For more in-depth documentation, `visit the Wiki! `_ +Documentation +------------- +`Visit the GitHub Wiki! `_ + Here are a few of the currently supported commands (note: support for these commands will vary from car to car): From 010ddd5b83ba7963e84d14d2cc60a8926702e545 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 26 Nov 2014 21:15:13 -0500 Subject: [PATCH 104/569] distilled the readme some more --- README.rst | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index f04f8f59..3a578bfd 100644 --- a/README.rst +++ b/README.rst @@ -1,28 +1,16 @@ python-OBD ========== -A python module for handling realtime sensor data from OBD-II vehicle ports. +A python module for handling realtime sensor data from OBD-II vehicle ports. Works with ELM327 OBD-II adapters, and is fit for the Raspberry Pi. Installation ------------ -Run the following command to download/install the latest release from pypi:: - $ pip install obd - -Dependencies ------------- - -+ pySerial -+ OBD-II adapter (ELM327 Bluetooth Adapter or ELM327 USB Cable) - - Basic Usage ----------- -After installing the library, simply import 'obd', and create a new OBD connection object. Commands can then be sent to the car using the `query` function:: - import obd connection = obd.OBD() # auto-connects to USB or RF port @@ -38,7 +26,8 @@ Documentation ------------- `Visit the GitHub Wiki! `_ - +Commands +-------- Here are a few of the currently supported commands (note: support for these commands will vary from car to car): + Calculated Engine Load @@ -63,6 +52,10 @@ Here are a few of the currently supported commands (note: support for these comm + Engine fuel rate + etc... (for a full list, see `commands.py `_) +License +------- +GNU GPL v2 + This library is forked from: + https://github.com/peterh/pyobd From 8c79236066317a99cf2fda2fc40a5c5c965bf2ac Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 26 Nov 2014 21:40:01 -0500 Subject: [PATCH 105/569] removed silly CLI, made names consistent --- obd/__main__.py | 121 --------------------------------------- obd/obd.py | 12 ++-- obd/utils.py | 2 +- tests/test_OBDCommand.py | 4 +- 4 files changed, 9 insertions(+), 130 deletions(-) delete mode 100644 obd/__main__.py diff --git a/obd/__main__.py b/obd/__main__.py deleted file mode 100644 index b79c1a48..00000000 --- a/obd/__main__.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python - -######################################################################## -# # -# python-OBD: A python OBD-II serial module derived from pyobd # -# # -# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # -# Copyright 2009 Secons Ltd. (www.obdtester.com) # -# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # -# # -######################################################################## -# # -# __main__.py # -# # -# This file is part of python-OBD (a derivative of pyOBD) # -# # -# python-OBD is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 2 of the License, or # -# (at your option) any later version. # -# # -# python-OBD is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with python-OBD. If not, see . # -# # -######################################################################## - -from sys import stdin, stdout, argv - -stdout.write("=============================\n") -stdout.write("Welcome to the python-OBD CLI\n") -stdout.write("=============================\n") - -def printHelp(): - stdout.write(""" -Built for python 2.7 - - python obd auto-detects serial port - python obd opens specified serial port - - OBD >>> sends command and prints result - OBD >>> : sends command and prints result - OBD >>> list lists all available commands - -""") - - -# catch the help flag -if ("help" in argv) or ("--help" in argv): - printHelp() - exit() - - -import obd -obd.debug.console = True -o = None - -# connect -if len(argv) == 1: - o = obd.OBD() # connect using scanSerial -elif len(argv) == 2: - o = obd.OBD(argv[1]) # connect to specified port -else: - stdout.write("Unknown command arguments. Please see 'python obd --help'") - - -def listCommands(o): - stdout.write("Supported Commands\n") - for c in o.supportedCommands: - stdout.write("\t%s:%s\t%s\n" % (c.mode, c.pid, c.desc)) - - - -if o is not None and o.is_connected(): - - listCommands(o) - - while True: - stdout.write("\nOBD >>> ") - i = stdin.readline() - i = i.strip() - i = i.upper() - - if i in ["EXIT", "QUIT"]: - stdout.write("Goodbye\n") - break; - - if i == "HELP": - printHelp() - continue - - if i == "LIST": - listCommands(o) - continue; - - parts = i.split(':') - - try: - # parse the user's input - command = None - - if len(parts) == 1: - command = obd.commands[parts[0]] - elif len(parts) == 2: - mode = int(parts[0], 16) - pid = int(parts[1], 16) - command = obd.commands[mode][pid] - - # send command and print result - if command is not None and o.has_command(command): - r = o.query(command) - stdout.write("\nDecoded Result:\n%s\n" % str(r)) - else: - stdout.write("Unsupported command: %s" % str(command)) - - except: - stdout.write("Could not parse command\n") diff --git a/obd/obd.py b/obd/obd.py index d3b88c61..fe2c5776 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -42,7 +42,7 @@ class OBD(): def __init__(self, portstr=None): self.port = None - self.supportedCommands = [] + self.supported_commands = [] # initialize by connecting and loading sensors debug("Starting python-OBD") @@ -86,7 +86,7 @@ def load_commands(self): debug("querying for supported PIDs (commands)...") - self.supportedCommands = [] + self.supported_commands = [] pid_getters = commands.pid_getters() @@ -98,7 +98,7 @@ def load_commands(self): response = self.query(get) # ask nicely - if response.isNull(): + if response.is_null(): continue supported = response.value # string of binary 01010101010101 @@ -116,13 +116,13 @@ def load_commands(self): # don't add PID getters to the command list if c not in pid_getters: - self.supportedCommands.append(c) + self.supported_commands.append(c) - debug("finished querying with %d commands supported" % len(self.supportedCommands)) + debug("finished querying with %d commands supported" % len(self.supported_commands)) def print_commands(self): - for c in self.supportedCommands: + for c in self.supported_commands: print str(c) def has_command(self, c): diff --git a/obd/utils.py b/obd/utils.py index 74f76ee8..f4980b3f 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -63,7 +63,7 @@ def __init__(self, raw_data=""): self.unit = Unit.NONE self.raw_data = raw_data - def isNull(self): + def is_null(self): return (self.value == "No Data") or (len(self.raw_data) == 0) def set(self, decode): diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index afb38b88..65256fbf 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -26,7 +26,7 @@ def test_data_stripping(): # name description mode cmd bytes decoder cmd = OBDCommand("Test", "example OBD command", "01", "00", 2, noop) r = cmd.compute("41 00 01 01\r\n") - assert not r.isNull() + assert not r.is_null() assert r.value == "0101" @@ -34,7 +34,7 @@ def test_data_not_hex(): # name description mode cmd bytes decoder cmd = OBDCommand("Test", "example OBD command", "01", "00", 2, noop) r = cmd.compute("41 00 wx yz\r\n") - assert r.isNull() + assert r.is_null() def test_data_length(): From bbc18eb8b9fd5cfcef533ec3c8ebcb47dd4c716d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 26 Nov 2014 21:59:56 -0500 Subject: [PATCH 106/569] tweaked readme --- README.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.rst b/README.rst index 3a578bfd..ec5a46fa 100644 --- a/README.rst +++ b/README.rst @@ -3,14 +3,17 @@ python-OBD A python module for handling realtime sensor data from OBD-II vehicle ports. Works with ELM327 OBD-II adapters, and is fit for the Raspberry Pi. + Installation ------------ $ pip install obd + Basic Usage ----------- +.. highlight:: python import obd connection = obd.OBD() # auto-connects to USB or RF port @@ -22,10 +25,12 @@ Basic Usage print(response.value) print(response.unit) + Documentation ------------- `Visit the GitHub Wiki! `_ + Commands -------- Here are a few of the currently supported commands (note: support for these commands will vary from car to car): From 7d1bfbfb656c080c56af1c16419c596ee9f400d2 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 26 Nov 2014 22:01:01 -0500 Subject: [PATCH 107/569] tweaked readme --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ec5a46fa..23ef154e 100644 --- a/README.rst +++ b/README.rst @@ -13,7 +13,8 @@ Installation Basic Usage ----------- -.. highlight:: python +:: + import obd connection = obd.OBD() # auto-connects to USB or RF port From c15ad2c8be403bf852265dc2496cf62d2443ba4d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 26 Nov 2014 22:01:28 -0500 Subject: [PATCH 108/569] tweaked readme --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 23ef154e..f5630ad6 100644 --- a/README.rst +++ b/README.rst @@ -7,6 +7,8 @@ A python module for handling realtime sensor data from OBD-II vehicle ports. Wor Installation ------------ +:: + $ pip install obd From 67a7b6f6c9a0e1a91d3153a2e95f2ff1dd674da4 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 27 Nov 2014 00:31:09 -0500 Subject: [PATCH 109/569] wrote basic query() test --- tests/test_OBD.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/test_OBD.py diff --git a/tests/test_OBD.py b/tests/test_OBD.py new file mode 100644 index 00000000..3930c791 --- /dev/null +++ b/tests/test_OBD.py @@ -0,0 +1,34 @@ + +import obd +from obd.utils import Response +from obd.commands import OBDCommand +from obd.decoders import noop + + +def test_query(): + # we don't need an actual serial connection + o = obd.OBD("/dev/null") + # forge our own command, to control the output + cmd = OBDCommand("", "", "01", "23", 2, noop) + + # forge data IO from the car by overwriting the get/send functions + + # buffers + toCar = [""] # needs to be inside mutable object to allow assignment in closure + fromCar = "" + + def send(cmd): + print cmd + toCar[0] = cmd + + o.port.send = send + o.port.get = lambda *args: fromCar + + + fromCar = "41 23 AB CD\r\r" + + r = o.query(cmd, True) + + assert toCar[0] == "0123" # verify that the command was sent correctly + assert r.raw_data == fromCar # verify that raw_data was stored in the Response + assert r.value == "ABCD" # verify that the response was parsed correctly From 25ae5537f990d52575a1c48d8d5ce2607e600833 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 27 Nov 2014 00:50:40 -0500 Subject: [PATCH 110/569] added response value tests --- tests/test_OBD.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 3930c791..9c6f4dd7 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -8,6 +8,7 @@ def test_query(): # we don't need an actual serial connection o = obd.OBD("/dev/null") + # forge our own command, to control the output cmd = OBDCommand("", "", "01", "23", 2, noop) @@ -18,17 +19,30 @@ def test_query(): fromCar = "" def send(cmd): - print cmd toCar[0] = cmd o.port.send = send o.port.get = lambda *args: fromCar + # test - fromCar = "41 23 AB CD\r\r" - - r = o.query(cmd, True) - + fromCar = "41 23 AB CD\r\r" # preset the response + r = o.query(cmd, True) # run assert toCar[0] == "0123" # verify that the command was sent correctly assert r.raw_data == fromCar # verify that raw_data was stored in the Response assert r.value == "ABCD" # verify that the response was parsed correctly + + fromCar = "NO DATA" + r = o.query(cmd, True) + assert r.raw_data == fromCar + assert r.is_null() + + fromCar = "totaly not hex!" + r = o.query(cmd, True) + assert r.raw_data == fromCar + assert r.is_null() + + fromCar = "" + r = o.query(cmd, True) + assert r.raw_data == fromCar + assert r.is_null() From 1227f34311dc6a6f5af671bef4b05dfd7893a48e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 27 Nov 2014 01:35:13 -0500 Subject: [PATCH 111/569] wrote tests for command tables --- tests/test_Commands.py | 56 ++++++++++++++++++++++++++++++++++++++++++ tests/test_OBD.py | 18 +++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 tests/test_Commands.py diff --git a/tests/test_Commands.py b/tests/test_Commands.py new file mode 100644 index 00000000..b5c45d72 --- /dev/null +++ b/tests/test_Commands.py @@ -0,0 +1,56 @@ + +import obd +from obd.decoders import pid + + +def test_list_integrity(): + for mode, cmds in enumerate(obd.commands.modes): + for pid, cmd in enumerate(cmds): + + # make sure the command tables are in mode & PID order + assert mode == cmd.get_mode_int() + assert pid == cmd.get_pid_int() + + # make sure all the fields are set + assert cmd.name != "" + assert cmd.desc != "" + assert (mode >= 1) and (mode <= 9) + assert (pid >= 0) and (pid <= 196) + assert cmd.bytes >= 0 + assert hasattr(cmd.decode, '__call__') + + +def test_unique_names(): + # make sure no two commands have the same name + names = {} + + for cmds in obd.commands.modes: + for cmd in cmds: + assert not names.has_key(cmd.name) + names[cmd.name] = True + + +def test_getitem_mode_pid(): + # ensure that obd.commands[mode][pid] works correctly + for cmds in obd.commands.modes: + for cmd in cmds: + mode = cmd.get_mode_int() + pid = cmd.get_pid_int() + assert cmd == obd.commands[mode][pid] + + +def test_getitem_name(): + # ensure that obd.commands[name] works correctly + for cmds in obd.commands.modes: + for cmd in cmds: + assert cmd == obd.commands[cmd.name] + + +def test_pid_getters(): + # ensure that all pid getters are found + pid_getters = obd.commands.pid_getters() + + for cmds in obd.commands.modes: + for cmd in cmds: + if cmd.decode == pid: + assert cmd in pid_getters diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 9c6f4dd7..4e4403a3 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -5,6 +5,13 @@ from obd.decoders import noop +def test_is_connected(): + o = obd.OBD("/dev/null") + assert not o.is_connected() + + # todo + + def test_query(): # we don't need an actual serial connection o = obd.OBD("/dev/null") @@ -26,6 +33,11 @@ def send(cmd): # test + fromCar = "41 23 AB CD\r\r" + r = o.query(cmd) # make sure unsupported commands don't send + assert toCar[0] == "" + assert r.is_null() + fromCar = "41 23 AB CD\r\r" # preset the response r = o.query(cmd, True) # run assert toCar[0] == "0123" # verify that the command was sent correctly @@ -37,7 +49,7 @@ def send(cmd): assert r.raw_data == fromCar assert r.is_null() - fromCar = "totaly not hex!" + fromCar = "totaly not hex!@#$" r = o.query(cmd, True) assert r.raw_data == fromCar assert r.is_null() @@ -46,3 +58,7 @@ def send(cmd): r = o.query(cmd, True) assert r.raw_data == fromCar assert r.is_null() + + +def test_load_commands(): + pass From 1d379e7d8b912e1ef4061938c6018cfa901a26f2 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 27 Nov 2014 01:37:50 -0500 Subject: [PATCH 112/569] simplified the getitem test --- tests/test_Commands.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/test_Commands.py b/tests/test_Commands.py index b5c45d72..03b085f5 100644 --- a/tests/test_Commands.py +++ b/tests/test_Commands.py @@ -30,19 +30,17 @@ def test_unique_names(): names[cmd.name] = True -def test_getitem_mode_pid(): - # ensure that obd.commands[mode][pid] works correctly +def test_getitem(): + # ensure that __getitem__ works correctly for cmds in obd.commands.modes: for cmd in cmds: + + # by [mode][pid] mode = cmd.get_mode_int() pid = cmd.get_pid_int() assert cmd == obd.commands[mode][pid] - -def test_getitem_name(): - # ensure that obd.commands[name] works correctly - for cmds in obd.commands.modes: - for cmd in cmds: + # by [name] assert cmd == obd.commands[cmd.name] From 2336c269f2eddff3d03b857c63644057574de51d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 27 Nov 2014 14:05:41 -0500 Subject: [PATCH 113/569] added variable response tests --- tests/test_OBD.py | 26 ++++++++++++++++---- tests/test_OBDCommand.py | 16 +++++++++++- tests/{test_Commands.py => test_commands.py} | 2 +- 3 files changed, 37 insertions(+), 7 deletions(-) rename tests/{test_Commands.py => test_commands.py} (96%) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 4e4403a3..eebb4c0e 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -15,11 +15,10 @@ def test_is_connected(): def test_query(): # we don't need an actual serial connection o = obd.OBD("/dev/null") - # forge our own command, to control the output cmd = OBDCommand("", "", "01", "23", 2, noop) - # forge data IO from the car by overwriting the get/send functions + # forge IO from the car by overwriting the get/send functions # buffers toCar = [""] # needs to be inside mutable object to allow assignment in closure @@ -31,29 +30,46 @@ def send(cmd): o.port.send = send o.port.get = lambda *args: fromCar - # test - + # make sure unsupported commands don't send fromCar = "41 23 AB CD\r\r" - r = o.query(cmd) # make sure unsupported commands don't send + r = o.query(cmd) assert toCar[0] == "" assert r.is_null() + # a correct command transaction fromCar = "41 23 AB CD\r\r" # preset the response r = o.query(cmd, True) # run assert toCar[0] == "0123" # verify that the command was sent correctly assert r.raw_data == fromCar # verify that raw_data was stored in the Response assert r.value == "ABCD" # verify that the response was parsed correctly + # response of greater length + fromCar = "41 23 AB CD EF\r\r" + r = o.query(cmd, True) + assert toCar[0] == "0123" + assert r.raw_data == fromCar + assert r.value == "ABCD" + + # response of greater length + fromCar = "41 23 AB\r\r" + r = o.query(cmd, True) + assert toCar[0] == "0123" + assert r.raw_data == fromCar + assert r.value == "AB00" + + # NO DATA response fromCar = "NO DATA" r = o.query(cmd, True) assert r.raw_data == fromCar assert r.is_null() + # malformed response fromCar = "totaly not hex!@#$" r = o.query(cmd, True) assert r.raw_data == fromCar assert r.is_null() + # no response fromCar = "" r = o.query(cmd, True) assert r.raw_data == fromCar diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index 65256fbf..879f5775 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -3,7 +3,7 @@ from obd.decoders import noop -def test_basic_OBDCommand(): +def test_constructor(): # name description mode cmd bytes decoder cmd = OBDCommand("Test", "example OBD command", "01", "23", 2, noop) assert cmd.name == "Test" @@ -22,6 +22,20 @@ def test_basic_OBDCommand(): assert cmd.supported == True +def test_clone(): + # name description mode cmd bytes decoder + cmd = OBDCommand("Test", "example OBD command", "01", "23", 2, noop) + other = cmd.clone() + + assert cmd.name == other.name + assert cmd.desc == other.desc + assert cmd.mode == other.mode + assert cmd.pid == other.pid + assert cmd.bytes == other.bytes + assert cmd.decode == other.decode + assert cmd.supported == cmd.supported + + def test_data_stripping(): # name description mode cmd bytes decoder cmd = OBDCommand("Test", "example OBD command", "01", "00", 2, noop) diff --git a/tests/test_Commands.py b/tests/test_commands.py similarity index 96% rename from tests/test_Commands.py rename to tests/test_commands.py index 03b085f5..1b18b831 100644 --- a/tests/test_Commands.py +++ b/tests/test_commands.py @@ -14,7 +14,7 @@ def test_list_integrity(): # make sure all the fields are set assert cmd.name != "" assert cmd.desc != "" - assert (mode >= 1) and (mode <= 9) + assert (mode >= 1) and (mode <= 9) assert (pid >= 0) and (pid <= 196) assert cmd.bytes >= 0 assert hasattr(cmd.decode, '__call__') From 37a087b0eecceaef0afc46064742f03ba7008466 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 27 Nov 2014 14:36:50 -0500 Subject: [PATCH 114/569] doing float comparison for decoder tests --- tests/test_decoders.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 11cb1da0..1bf5de0a 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -3,12 +3,22 @@ import obd.decoders as d +def float_equals(d1, d2): + values_match = (abs(d1[0] - d2[0]) < 0.02) + units_match = (d1[1] == d2[1]) + return values_match and units_match + + + + + def test_noop(): assert d.noop("No Operation") == ("No Operation", Unit.NONE) def test_pid(): assert d.pid("00000000") == ("00000000000000000000000000000000", Unit.NONE) assert d.pid("F00AA00F") == ("11110000000010101010000000001111", Unit.NONE) + assert d.pid("11") == ("00010001", Unit.NONE) def test_count(): assert d.count("0") == (0, Unit.COUNT) @@ -20,9 +30,9 @@ def test_percent(): assert d.percent("FF") == (100.0, Unit.PERCENT) def test_percent_centered(): - assert d.percent_centered("00") == (-100.0, Unit.PERCENT) - assert d.percent_centered("80") == (0.0, Unit.PERCENT) - #assert d.percent_centered("FF") == (100.0, Unit.PERCENT) # returns 99.21875, need float checking or better math + assert d.percent_centered("00") == (-100.0, Unit.PERCENT) + assert d.percent_centered("80") == (0.0, Unit.PERCENT) + assert float_equals(d.percent_centered("FF"), (99.2, Unit.PERCENT)) def test_temp(): assert d.temp("00") == (-40, Unit.C) @@ -34,20 +44,20 @@ def test_catalyst_temp(): assert d.catalyst_temp("FFFF") == (6513.5, Unit.C) def test_current_centered(): - assert d.current_centered("00000000") == (-128.0, Unit.MA) - assert d.current_centered("00008000") == (0.0, Unit.MA) - #assert d.current_centered("0000FFFF") == (128.0, Unit.MA) # returns 127.99609375, need float checking or better math - assert d.current_centered("ABCD8000") == (0.0, Unit.MA) # first 2 bytes are unused (should be disregarded) + assert d.current_centered("00000000") == (-128.0, Unit.MA) + assert d.current_centered("00008000") == (0.0, Unit.MA) + assert float_equals(d.current_centered("0000FFFF"), (128.0, Unit.MA)) + assert d.current_centered("ABCD8000") == (0.0, Unit.MA) # first 2 bytes are unused (should be disregarded) def test_sensor_voltage(): assert d.sensor_voltage("0000") == (0.0, Unit.VOLT) assert d.sensor_voltage("FFFF") == (1.275, Unit.VOLT) def test_sensor_voltage_big(): - assert d.sensor_voltage_big("00000000") == (0.0, Unit.VOLT) - #assert d.sensor_voltage_big("00008000") == (4.0, Unit.VOLT) # returns 127.99609375, need float checking or better math - assert d.sensor_voltage_big("0000FFFF") == (8.0, Unit.VOLT) - assert d.sensor_voltage_big("ABCD0000") == (0.0, Unit.VOLT) # first 2 bytes are unused (should be disregarded) + assert d.sensor_voltage_big("00000000") == (0.0, Unit.VOLT) + assert float_equals(d.sensor_voltage_big("00008000"), (4.0, Unit.VOLT)) + assert d.sensor_voltage_big("0000FFFF") == (8.0, Unit.VOLT) + assert d.sensor_voltage_big("ABCD0000") == (0.0, Unit.VOLT) # first 2 bytes are unused (should be disregarded) def test_fuel_pressure(): assert d.fuel_pressure("00") == (0, Unit.KPA) @@ -92,8 +102,8 @@ def test_timing_advance(): assert d.timing_advance("FF") == (63.5, Unit.DEGREES) def test_inject_timing(): - assert d.inject_timing("0000") == (-210, Unit.DEGREES) - #assert d.inject_timing("FFFF") == (301, Unit.DEGREES) + assert d.inject_timing("0000") == (-210, Unit.DEGREES) + assert float_equals(d.inject_timing("FFFF"), (302, Unit.DEGREES)) def test_maf(): assert d.maf("0000") == (0.0, Unit.GPS) From 6f9f21382567a18d854c7ab91886f3effd1040a0 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 29 Nov 2014 15:23:35 -0500 Subject: [PATCH 115/569] made Async a subclass --- obd/async.py | 38 +++++++++++++++++--------------------- obd/obd.py | 2 +- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/obd/async.py b/obd/async.py index f8b73cc5..787a534f 100644 --- a/obd/async.py +++ b/obd/async.py @@ -35,11 +35,11 @@ from commands import OBDCommand -class Async(): - """ class representing an OBD-II connection """ +class Async(obd.OBD): + """ subclass representing an OBD-II connection """ def __init__(self, portstr=None): - self.connection = obd.OBD(portstr) + super(Async, self).__init__(portstr) self.commands = {} # key = OBDCommand, value = Response self.thread = None self.running = False @@ -47,8 +47,9 @@ def __init__(self, portstr=None): def start(self): self.running = True - if self.connection.is_connected(): - self.thread = threading.Thread(target=self.run, args=(self.connection,)) + if self.is_connected(): + self.thread = threading.Thread(target=self.run) + self.thread.daemon = True self.thread.start() def stop(self): @@ -59,25 +60,20 @@ def stop(self): def close(self): self.stop() - self.connection.close() + self.close() - def watch(self, *commands): + def watch(self, c): - errors = [] + if not isinstance(c, OBDCommand): + return False - for c in commands: - if not isinstance(c, OBDCommand): - errors.append(c) - continue + if not self.has_command(c): + return False - if not self.connection.has_command(c): - errors.append(c) - continue + if not self.commands.has_key(c): + self.commands[c] = Response() # give it an initial value - if not self.commands.has_key(c): - self.commands[c] = Response() # give it an initial value - - return errors + return True def unwatch(self, c): self.commands.pop(c, None) @@ -88,13 +84,13 @@ def get(self, c): else: return Response() - def run(self, connection): + def run(self): # loop until the stop signal is recieved while self.running: if len(self.commands) > 0: # loop over the requested commands, and collect the result for c in self.commands: - self.commands[c] = connection.query(c) + self.commands[c] = self.query(c) else: time.sleep(1) diff --git a/obd/obd.py b/obd/obd.py index ba958ee1..d05e5370 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -37,7 +37,7 @@ -class OBD(): +class OBD(object): """ class representing an OBD-II connection with it's assorted sensors """ def __init__(self, portstr=None): From bccff6dcd9672f824231343ae306ee3a6b9e7f6d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 29 Nov 2014 18:34:03 -0500 Subject: [PATCH 116/569] Async merge fixes --- obd/async.py | 28 ++++++++++++++++++++-------- obd/commands.py | 2 +- obd/debug.py | 4 ++-- obd/obd.py | 27 ++++++++++++++++++--------- obd/port.py | 2 +- 5 files changed, 42 insertions(+), 21 deletions(-) diff --git a/obd/async.py b/obd/async.py index 787a534f..3a2ce7aa 100644 --- a/obd/async.py +++ b/obd/async.py @@ -33,6 +33,8 @@ import threading from utils import Response from commands import OBDCommand +from debug import debug + class Async(obd.OBD): @@ -45,40 +47,50 @@ def __init__(self, portstr=None): self.running = False self.start() + def start(self): - self.running = True if self.is_connected(): + debug("Starting async thread") + self.running = True self.thread = threading.Thread(target=self.run) self.thread.daemon = True self.thread.start() + else: + debug("Async thread not started because no connection was made") + def stop(self): - self.running = False if self.thread is not None: + debug("Stopping async thread...") + self.running = False self.thread.join() self.thread = None + debug("Async thread stopped") + def close(self): self.stop() self.close() - def watch(self, c): - if not isinstance(c, OBDCommand): - return False + def watch(self, c, force=False): - if not self.has_command(c): + if not (self.has_command(c) or force): + debug("'%s' is not supported" % str(c), True) return False if not self.commands.has_key(c): + debug("Watching command: %s" % str(c)) self.commands[c] = Response() # give it an initial value return True + def unwatch(self, c): + debug("Unwatching command: %s" % str(c)) self.commands.pop(c, None) - def get(self, c): + def query(self, c): if self.commands.has_key(c): return self.commands[c] else: @@ -91,6 +103,6 @@ def run(self): if len(self.commands) > 0: # loop over the requested commands, and collect the result for c in self.commands: - self.commands[c] = self.query(c) + self.commands[c] = self.send(c) else: time.sleep(1) diff --git a/obd/commands.py b/obd/commands.py index 56b1431d..1ef9567a 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -293,7 +293,7 @@ def set_supported(self, mode, pid, v): if (mode < len(self.modes)) and (pid < len(self.modes[mode])): self.modes[mode][pid].supported = v else: - debug("set_supported only accepts boolean values", True) + debug("set_supported() only accepts boolean values", True) # checks for existance of int mode and int pid def has(self, mode, pid): diff --git a/obd/debug.py b/obd/debug.py index 3c8ba9d1..51cf3c88 100644 --- a/obd/debug.py +++ b/obd/debug.py @@ -30,13 +30,13 @@ class Debug(): def __init__(self): - self.console = True + self.console = False self.handler = None def __call__(self, msg, forcePrint=False): if self.console or forcePrint: - print msg + print(msg) if hasattr(self.handler, '__call__'): self.handler(msg) diff --git a/obd/obd.py b/obd/obd.py index 1095cb46..8eac706d 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -77,6 +77,7 @@ def connect(self, portstr=None): def close(self): if self.is_connected(): + debug("Closing connection") self.port.close() self.port = None @@ -108,7 +109,7 @@ def load_commands(self): if not self.has_command(get): continue - response = self.query(get) # ask nicely + response = self.send(get) # ask nicely if response.is_null(): continue @@ -137,26 +138,34 @@ def print_commands(self): for c in self.supported_commands: print str(c) + def has_command(self, c): return commands.has(c.get_mode_int(), c.get_pid_int()) and c.supported - def query(self, command, force=False): - """ send the given command, retrieve response, and parse response """ + + def send(self, c): + """ send the given command, retrieve and parse response """ # check for a connection if not self.is_connected(): debug("Query failed, no connection available", True) return Response() # return empty response + # send the query + debug("Sending command: %s" % str(c)) + self.port.send(c.get_command()) # send command to the port + return c.compute(self.port.get()) # get the data, and compute a response object + + + def query(self, c, force=False): + # check that the command is supported - if not (self.has_command(command) or force): - debug("'%s' is not supported" % str(command), True) + if not (self.has_command(c) or force): + debug("'%s' is not supported" % str(c), True) return Response() # return empty response + else: + return self.send(c) - # send the query - debug("Sending command: %s" % str(command)) - self.port.send(command.get_command()) # send command to the port - return command.compute(self.port.get()) # get the data, and compute a response object ''' def query_DTC(self): diff --git a/obd/port.py b/obd/port.py index d7bf0040..1a9574a5 100644 --- a/obd/port.py +++ b/obd/port.py @@ -140,7 +140,7 @@ def send(self, cmd): def get(self): """Internal use only: not a public interface""" - attempts = 1 + attempts = 2 result = "" if self.port is not None: From 1b92f3825f2e513768863a3682bf8e6851378eab Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 29 Nov 2014 18:47:03 -0500 Subject: [PATCH 117/569] minor tweaks, fixed query test --- obd/async.py | 6 +++++- obd/obd.py | 4 ++-- tests/test_OBD.py | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/obd/async.py b/obd/async.py index 3a2ce7aa..273194ea 100644 --- a/obd/async.py +++ b/obd/async.py @@ -90,18 +90,22 @@ def unwatch(self, c): debug("Unwatching command: %s" % str(c)) self.commands.pop(c, None) + def query(self, c): if self.commands.has_key(c): return self.commands[c] else: return Response() + def run(self): + """ Daemon thread """ + # loop until the stop signal is recieved while self.running: if len(self.commands) > 0: - # loop over the requested commands, and collect the result + # loop over the requested commands, send, and collect the result for c in self.commands: self.commands[c] = self.send(c) else: diff --git a/obd/obd.py b/obd/obd.py index 8eac706d..94ef7b27 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -29,7 +29,6 @@ ######################################################################## import time - from port import OBDPort, State from commands import commands from utils import scanSerial, Response @@ -158,7 +157,8 @@ def send(self, c): def query(self, c, force=False): - + """ facade 'send' command, protects against sending unsupported commands """ + # check that the command is supported if not (self.has_command(c) or force): debug("'%s' is not supported" % str(c), True) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index eebb4c0e..0951feac 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -27,6 +27,7 @@ def test_query(): def send(cmd): toCar[0] = cmd + o.is_connected = lambda *args: True o.port.send = send o.port.get = lambda *args: fromCar From 73cf338523dfe69c996b0e37f53ac93a3091e654 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 29 Nov 2014 19:13:24 -0500 Subject: [PATCH 118/569] allow watch() and unwatch() to start/stop the thread --- obd/async.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/obd/async.py b/obd/async.py index 273194ea..7a3298cb 100644 --- a/obd/async.py +++ b/obd/async.py @@ -45,7 +45,6 @@ def __init__(self, portstr=None): self.commands = {} # key = OBDCommand, value = Response self.thread = None self.running = False - self.start() def start(self): @@ -77,19 +76,24 @@ def watch(self, c, force=False): if not (self.has_command(c) or force): debug("'%s' is not supported" % str(c), True) - return False if not self.commands.has_key(c): debug("Watching command: %s" % str(c)) self.commands[c] = Response() # give it an initial value - return True + # if not already running, start + if (not self.running) and (len(self.commands) > 0): + self.start() def unwatch(self, c): debug("Unwatching command: %s" % str(c)) self.commands.pop(c, None) + # if already running, start + if self.running and (len(self.commands) == 0): + self.stop() + def query(self, c): if self.commands.has_key(c): @@ -109,4 +113,4 @@ def run(self): for c in self.commands: self.commands[c] = self.send(c) else: - time.sleep(1) + time.sleep(1) # hopefully, this should never happen thanks to the gaurds in watch() and unwatch() From ee3d3c00394d8225905b89b7793c310b7cedcec9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 29 Nov 2014 21:02:16 -0500 Subject: [PATCH 119/569] call the super class when closing --- obd/async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/async.py b/obd/async.py index 7a3298cb..918c4b33 100644 --- a/obd/async.py +++ b/obd/async.py @@ -69,7 +69,7 @@ def stop(self): def close(self): self.stop() - self.close() + super(Async, self).close() def watch(self, c, force=False): From 612fc053b361e858aa446f4b13fefae7a17897ae Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 1 Dec 2014 13:07:58 -0500 Subject: [PATCH 120/569] added callback functionality to async --- obd/async.py | 34 ++++++++++++++++++++++++++-------- tests/test_OBD.py | 12 ++++++------ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/obd/async.py b/obd/async.py index 918c4b33..56e7a60f 100644 --- a/obd/async.py +++ b/obd/async.py @@ -42,9 +42,10 @@ class Async(obd.OBD): def __init__(self, portstr=None): super(Async, self).__init__(portstr) - self.commands = {} # key = OBDCommand, value = Response - self.thread = None - self.running = False + self.commands = {} # key = OBDCommand, value = Response + self.callbacks = {} # key = OBDCommand, value = Callback + self.thread = None + self.running = False def start(self): @@ -72,15 +73,24 @@ def close(self): super(Async, self).close() - def watch(self, c, force=False): + def watch(self, c, callback=None, force=False): if not (self.has_command(c) or force): debug("'%s' is not supported" % str(c), True) + # store the command if not self.commands.has_key(c): debug("Watching command: %s" % str(c)) self.commands[c] = Response() # give it an initial value + # store the callback + if (callback is not None) and (not self.callbacks.has_key(c)): + if hasattr(callback, "__call__"): + debug("subscribing callback for command: %s" % str(c)) + self.callbacks[c] = callback + else: + debug("all callbacks must be callable") + # if not already running, start if (not self.running) and (len(self.commands) > 0): self.start() @@ -90,7 +100,7 @@ def unwatch(self, c): debug("Unwatching command: %s" % str(c)) self.commands.pop(c, None) - # if already running, start + # if already running, stop if self.running and (len(self.commands) == 0): self.stop() @@ -109,8 +119,16 @@ def run(self): while self.running: if len(self.commands) > 0: - # loop over the requested commands, send, and collect the result + # loop over the requested commands, send, and collect the response for c in self.commands: - self.commands[c] = self.send(c) + r = self.send(c) + + # store the response + self.commands[c] = r + + # fire the callback + if self.callbacks.has_key(c): + self.callbacks[c](r) + else: - time.sleep(1) # hopefully, this should never happen thanks to the gaurds in watch() and unwatch() + time.sleep(1) # hopefully this should never happen, thanks to the gaurds in watch() and unwatch() diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 0951feac..6d921117 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -39,40 +39,40 @@ def send(cmd): # a correct command transaction fromCar = "41 23 AB CD\r\r" # preset the response - r = o.query(cmd, True) # run + r = o.query(cmd, force=True) # run assert toCar[0] == "0123" # verify that the command was sent correctly assert r.raw_data == fromCar # verify that raw_data was stored in the Response assert r.value == "ABCD" # verify that the response was parsed correctly # response of greater length fromCar = "41 23 AB CD EF\r\r" - r = o.query(cmd, True) + r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.raw_data == fromCar assert r.value == "ABCD" # response of greater length fromCar = "41 23 AB\r\r" - r = o.query(cmd, True) + r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.raw_data == fromCar assert r.value == "AB00" # NO DATA response fromCar = "NO DATA" - r = o.query(cmd, True) + r = o.query(cmd, force=True) assert r.raw_data == fromCar assert r.is_null() # malformed response fromCar = "totaly not hex!@#$" - r = o.query(cmd, True) + r = o.query(cmd, force=True) assert r.raw_data == fromCar assert r.is_null() # no response fromCar = "" - r = o.query(cmd, True) + r = o.query(cmd, force=True) assert r.raw_data == fromCar assert r.is_null() From 83df4c1c3eb61aa44c060c345f3de049a17a0147 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 1 Dec 2014 13:22:06 -0500 Subject: [PATCH 121/569] unwired callbacks on unwatch, added timestamps --- obd/async.py | 1 + obd/utils.py | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/obd/async.py b/obd/async.py index 56e7a60f..f3cfad3b 100644 --- a/obd/async.py +++ b/obd/async.py @@ -99,6 +99,7 @@ def watch(self, c, callback=None, force=False): def unwatch(self, c): debug("Unwatching command: %s" % str(c)) self.commands.pop(c, None) + self.callbacks.pop(c, None) # if already running, stop if self.running and (len(self.commands) == 0): diff --git a/obd/utils.py b/obd/utils.py index f4980b3f..00c825db 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -31,6 +31,7 @@ import serial import errno import string +import time from debug import debug @@ -59,12 +60,13 @@ class Unit: class Response(): def __init__(self, raw_data=""): - self.value = "No Data" - self.unit = Unit.NONE + self.value = None + self.unit = Unit.NONE self.raw_data = raw_data + self.time = time.time() def is_null(self): - return (self.value == "No Data") or (len(self.raw_data) == 0) + return (len(self.raw_data) == 0) or (self.value == None) def set(self, decode): self.value = decode[0] From 306cccc354cfc747041a302ad50c293b01af7b7f Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 2 Dec 2014 13:21:52 -0500 Subject: [PATCH 122/569] simplified callback checking --- obd/async.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/obd/async.py b/obd/async.py index f3cfad3b..75a3cbaf 100644 --- a/obd/async.py +++ b/obd/async.py @@ -84,12 +84,9 @@ def watch(self, c, callback=None, force=False): self.commands[c] = Response() # give it an initial value # store the callback - if (callback is not None) and (not self.callbacks.has_key(c)): - if hasattr(callback, "__call__"): - debug("subscribing callback for command: %s" % str(c)) - self.callbacks[c] = callback - else: - debug("all callbacks must be callable") + if hasattr(callback, "__call__") and (not self.callbacks.has_key(c)): + debug("subscribing callback for command: %s" % str(c)) + self.callbacks[c] = callback # if not already running, start if (not self.running) and (len(self.commands) > 0): From cdce2ee9971bce9047a1b2488a0d92e69d8766b1 Mon Sep 17 00:00:00 2001 From: Andrew AJ Mandula Date: Thu, 4 Dec 2014 17:53:33 -0500 Subject: [PATCH 123/569] added Windows port support --- obd/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/obd/utils.py b/obd/utils.py index 00c825db..59e8f026 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -161,6 +161,12 @@ def scanSerial(): if tryPort(portStr): available.append(portStr) + #Enable for Windows + for i in range(256): + portStr = "\\.\COM%d" % i + if tryPort(portStr): + available.append(portStr) + # Enable obdsim ''' for i in range(256): From 5d6423922566066269326c931c350d58410dd147 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 5 Dec 2014 23:11:47 -0500 Subject: [PATCH 124/569] fixed threading problem with command dict resizing during iteration --- obd/async.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/obd/async.py b/obd/async.py index 75a3cbaf..f274a276 100644 --- a/obd/async.py +++ b/obd/async.py @@ -77,6 +77,12 @@ def watch(self, c, callback=None, force=False): if not (self.has_command(c) or force): debug("'%s' is not supported" % str(c), True) + return + + # if running, the daemon thread must be stopped before altering the command dict + was_running = self.running + if self.running: + self.stop() # store the command if not self.commands.has_key(c): @@ -88,19 +94,25 @@ def watch(self, c, callback=None, force=False): debug("subscribing callback for command: %s" % str(c)) self.callbacks[c] = callback - # if not already running, start - if (not self.running) and (len(self.commands) > 0): + # start if neccessary + if was_running: self.start() def unwatch(self, c): + + # if running, the daemon thread must be stopped before altering the command dict + was_running = self.running + if self.running: + self.stop() + debug("Unwatching command: %s" % str(c)) self.commands.pop(c, None) self.callbacks.pop(c, None) - # if already running, stop - if self.running and (len(self.commands) == 0): - self.stop() + # start if neccessary + if was_running: + self.start() def query(self, c): @@ -124,9 +136,9 @@ def run(self): # store the response self.commands[c] = r - # fire the callback - if self.callbacks.has_key(c): + # fire the callback, if we have one + if c in self.callbacks: self.callbacks[c](r) else: - time.sleep(1) # hopefully this should never happen, thanks to the gaurds in watch() and unwatch() + time.sleep(1) # idle until the user calls stop() or watch() From 3f9d7f0483c725c4599366bc728e51dddd73de77 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 5 Dec 2014 23:57:08 -0500 Subject: [PATCH 125/569] switched class name from OBD to Obd --- obd/__init__.py | 2 +- obd/async.py | 9 ++++++++- obd/obd.py | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/obd/__init__.py b/obd/__init__.py index 11a3ca9e..8bc35a1a 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -28,7 +28,7 @@ # # ######################################################################## -from obd import OBD +from obd import Obd from commands import commands, OBDCommand from utils import scanSerial, Unit from debug import debug diff --git a/obd/async.py b/obd/async.py index f274a276..6a1584af 100644 --- a/obd/async.py +++ b/obd/async.py @@ -37,7 +37,7 @@ -class Async(obd.OBD): +class Async(obd.Obd): """ subclass representing an OBD-II connection """ def __init__(self, portstr=None): @@ -115,6 +115,13 @@ def unwatch(self, c): self.start() + def unwatch_all(self): + commands = self.commands.keys() + + for c in commands: + self.unwatch(c) + + def query(self, c): if self.commands.has_key(c): return self.commands[c] diff --git a/obd/obd.py b/obd/obd.py index 94ef7b27..f1798238 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -36,7 +36,7 @@ -class OBD(object): +class Obd(object): """ class representing an OBD-II connection with it's assorted sensors """ def __init__(self, portstr=None): From 98423d2c9b5d0d924d193a253db0f869095228fa Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 15 Dec 2014 17:26:24 -0500 Subject: [PATCH 126/569] decided against the name change, its an acronym, it should stay caps --- obd/__init__.py | 2 +- obd/async.py | 2 +- obd/obd.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/obd/__init__.py b/obd/__init__.py index 8bc35a1a..11a3ca9e 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -28,7 +28,7 @@ # # ######################################################################## -from obd import Obd +from obd import OBD from commands import commands, OBDCommand from utils import scanSerial, Unit from debug import debug diff --git a/obd/async.py b/obd/async.py index 6a1584af..85212920 100644 --- a/obd/async.py +++ b/obd/async.py @@ -37,7 +37,7 @@ -class Async(obd.Obd): +class Async(obd.OBD): """ subclass representing an OBD-II connection """ def __init__(self, portstr=None): diff --git a/obd/obd.py b/obd/obd.py index f1798238..94ef7b27 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -36,7 +36,7 @@ -class Obd(object): +class OBD(object): """ class representing an OBD-II connection with it's assorted sensors """ def __init__(self, portstr=None): From ebeb5759e0c3e780771878913e69e6760e64cf94 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 15 Dec 2014 22:08:35 -0500 Subject: [PATCH 127/569] allow for multiple callbacks per command --- obd/async.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/obd/async.py b/obd/async.py index 85212920..4cd5694a 100644 --- a/obd/async.py +++ b/obd/async.py @@ -43,7 +43,7 @@ class Async(obd.OBD): def __init__(self, portstr=None): super(Async, self).__init__(portstr) self.commands = {} # key = OBDCommand, value = Response - self.callbacks = {} # key = OBDCommand, value = Callback + self.callbacks = {} # key = OBDCommand, value = list of Functions self.thread = None self.running = False @@ -84,22 +84,23 @@ def watch(self, c, callback=None, force=False): if self.running: self.stop() - # store the command + # new command being watched, store the command if not self.commands.has_key(c): debug("Watching command: %s" % str(c)) self.commands[c] = Response() # give it an initial value + self.callbacks[c] = [] # create an empty list - # store the callback - if hasattr(callback, "__call__") and (not self.callbacks.has_key(c)): + # if a callback was given, push it + if hasattr(callback, "__call__") and (callback not in self.callbacks[c]): debug("subscribing callback for command: %s" % str(c)) - self.callbacks[c] = callback + self.callbacks[c].append(callback) # start if neccessary if was_running: self.start() - def unwatch(self, c): + def unwatch(self, c, callback=None): # if running, the daemon thread must be stopped before altering the command dict was_running = self.running @@ -107,8 +108,19 @@ def unwatch(self, c): self.stop() debug("Unwatching command: %s" % str(c)) - self.commands.pop(c, None) - self.callbacks.pop(c, None) + + # if a callback was specified, only remove the callback + if hasattr(callback, "__call__") and (callback in self.callbacks[c]): + self.callbacks[c].remove(callback) + + # if no more callbacks are left, remove the command entirely + if len(self.callbacks[c]) === 0: + self.commands.pop(c, None) + else: + # no callback was specified, pop everything + self.callbacks.pop(c, None) + self.commands.pop(c, None) + # start if neccessary if was_running: From f190039df302fc9da782dfa2b49a914cc783e53d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 15 Dec 2014 22:54:55 -0500 Subject: [PATCH 128/569] calling unwatch can have the affect of only removing the callback --- obd/async.py | 40 +++++++++++++++++++++++++--------------- obd/obd.py | 1 + 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/obd/async.py b/obd/async.py index 4cd5694a..ba9e8b3b 100644 --- a/obd/async.py +++ b/obd/async.py @@ -109,17 +109,18 @@ def unwatch(self, c, callback=None): debug("Unwatching command: %s" % str(c)) - # if a callback was specified, only remove the callback - if hasattr(callback, "__call__") and (callback in self.callbacks[c]): - self.callbacks[c].remove(callback) - - # if no more callbacks are left, remove the command entirely - if len(self.callbacks[c]) === 0: + if c in self.commands: + # if a callback was specified, only remove the callback + if hasattr(callback, "__call__") and (callback in self.callbacks[c]): + self.callbacks[c].remove(callback) + + # if no more callbacks are left, remove the command entirely + if len(self.callbacks[c]) == 0: + self.commands.pop(c, None) + else: + # no callback was specified, pop everything + self.callbacks.pop(c, None) self.commands.pop(c, None) - else: - # no callback was specified, pop everything - self.callbacks.pop(c, None) - self.commands.pop(c, None) # start if neccessary @@ -128,10 +129,19 @@ def unwatch(self, c, callback=None): def unwatch_all(self): - commands = self.commands.keys() + debug("Unwatching all") + + # if running, the daemon thread must be stopped before altering the command dict + was_running = self.running + if self.running: + self.stop() + + self.commands = {} + self.callbacks = {} - for c in commands: - self.unwatch(c) + # start if neccessary + if was_running: + self.start() def query(self, c): @@ -156,8 +166,8 @@ def run(self): self.commands[c] = r # fire the callback, if we have one - if c in self.callbacks: - self.callbacks[c](r) + for callback in self.callbacks[c]: + callback(r) else: time.sleep(1) # idle until the user calls stop() or watch() diff --git a/obd/obd.py b/obd/obd.py index 94ef7b27..cc64907f 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -45,6 +45,7 @@ def __init__(self, portstr=None): # initialize by connecting and loading sensors self.connect(portstr) + debug("========================================") def connect(self, portstr=None): From 08ca3c6440a8e002946ec81ff14232f8a70d5116 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 16 Dec 2014 00:04:33 -0500 Subject: [PATCH 129/569] made unit types more consistant and more useful --- obd/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/obd/utils.py b/obd/utils.py index 59e8f026..a8d08c75 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -39,7 +39,7 @@ class Unit: NONE = None RATIO = "Ratio" COUNT = "Count" - PERCENT = "Percent" + PERCENT = "%" RPM = "RPM" VOLT = "Volt" F = "F" @@ -48,9 +48,9 @@ class Unit: MIN = "Minute" PA = "Pa" KPA = "kPa" - PSI = "PSI" - KPH = "Kilometers per Hour" - MPH = "Miles per Hour" + PSI = "psi" + KPH = "kph" + MPH = "mph" DEGREES = "Degrees" GPS = "Grams per Second" MA = "mA" From 74208a0d6d4f5e10e0823647c7db56c0a2250ee9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 16 Dec 2014 00:10:50 -0500 Subject: [PATCH 130/569] bump to 0.2.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9f4751bd..17b12e38 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="obd", - version="0.1.0", + version="0.2.0", description=("Serial module for handling live sensor data from a vehicle's OBD-II port"), classifiers=[ "Operating System :: POSIX :: Linux", From a69b362f56ee65eb85682165cc392131a390ac9e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 17 Dec 2014 11:36:31 -0500 Subject: [PATCH 131/569] linked to the wiki in the readme --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index f5630ad6..0529bd41 100644 --- a/README.rst +++ b/README.rst @@ -58,7 +58,7 @@ Here are a few of the currently supported commands (note: support for these comm + Time since trouble codes cleared + Hybrid battery pack remaining life + Engine fuel rate -+ etc... (for a full list, see `commands.py `_) ++ etc... (for a full list, see `the wiki `_) License ------- From 3c3fb5fd4ddca5dfe2d95669eb08dba6dc59ed66 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 18 Dec 2014 13:28:38 -0500 Subject: [PATCH 132/569] fixed capitalization error in command names --- obd/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 1ef9567a..d09e464c 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -200,9 +200,9 @@ def __eq__(self, other): OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , "01", "53", 2, abs_evap_pressure ), OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , "01", "54", 2, evap_pressure_alt ), OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , "01", "55", 2, percent_centered ), # todo: decode seconds value for banks 3 and 4 - OBDCommand("Long_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , "01", "56", 2, percent_centered ), + OBDCommand("LONG_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , "01", "56", 2, percent_centered ), OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , "01", "57", 2, percent_centered ), - OBDCommand("Long_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , "01", "58", 2, percent_centered ), + OBDCommand("LONG_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , "01", "58", 2, percent_centered ), OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , "01", "59", 2, fuel_pres_direct ), OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , "01", "5A", 1, percent ), OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , "01", "5B", 1, percent ), From 896908054a42a86b2adabceb46a7de7221fcb62a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 27 Dec 2014 15:13:22 -0500 Subject: [PATCH 133/569] removed auto starting/stopping, that is more efficiently done by the app programmer --- obd/async.py | 93 ++++++++++++++++++++++------------------------------ 1 file changed, 39 insertions(+), 54 deletions(-) diff --git a/obd/async.py b/obd/async.py index ba9e8b3b..6a0559a6 100644 --- a/obd/async.py +++ b/obd/async.py @@ -75,73 +75,58 @@ def close(self): def watch(self, c, callback=None, force=False): - if not (self.has_command(c) or force): - debug("'%s' is not supported" % str(c), True) - return - - # if running, the daemon thread must be stopped before altering the command dict - was_running = self.running + # the dict shouldn't be changed while the daemon thread is iterating if self.running: - self.stop() + debug("Can't watch() while running, please use stop()", True) + else: - # new command being watched, store the command - if not self.commands.has_key(c): - debug("Watching command: %s" % str(c)) - self.commands[c] = Response() # give it an initial value - self.callbacks[c] = [] # create an empty list + if not (self.has_command(c) or force): + debug("'%s' is not supported" % str(c), True) + return - # if a callback was given, push it - if hasattr(callback, "__call__") and (callback not in self.callbacks[c]): - debug("subscribing callback for command: %s" % str(c)) - self.callbacks[c].append(callback) + # new command being watched, store the command + if not self.commands.has_key(c): + debug("Watching command: %s" % str(c)) + self.commands[c] = Response() # give it an initial value + self.callbacks[c] = [] # create an empty list - # start if neccessary - if was_running: - self.start() + # if a callback was given, push it + if hasattr(callback, "__call__") and (callback not in self.callbacks[c]): + debug("subscribing callback for command: %s" % str(c)) + self.callbacks[c].append(callback) def unwatch(self, c, callback=None): - # if running, the daemon thread must be stopped before altering the command dict - was_running = self.running + # the dict shouldn't be changed while the daemon thread is iterating if self.running: - self.stop() - - debug("Unwatching command: %s" % str(c)) - - if c in self.commands: - # if a callback was specified, only remove the callback - if hasattr(callback, "__call__") and (callback in self.callbacks[c]): - self.callbacks[c].remove(callback) - - # if no more callbacks are left, remove the command entirely - if len(self.callbacks[c]) == 0: + debug("Can't unwatch() while running, please use stop()", True) + else: + debug("Unwatching command: %s" % str(c)) + + if c in self.commands: + # if a callback was specified, only remove the callback + if hasattr(callback, "__call__") and (callback in self.callbacks[c]): + self.callbacks[c].remove(callback) + + # if no more callbacks are left, remove the command entirely + if len(self.callbacks[c]) == 0: + self.commands.pop(c, None) + else: + # no callback was specified, pop everything + self.callbacks.pop(c, None) self.commands.pop(c, None) - else: - # no callback was specified, pop everything - self.callbacks.pop(c, None) - self.commands.pop(c, None) - - - # start if neccessary - if was_running: - self.start() def unwatch_all(self): - debug("Unwatching all") - # if running, the daemon thread must be stopped before altering the command dict - was_running = self.running + # the dict shouldn't be changed while the daemon thread is iterating if self.running: - self.stop() - - self.commands = {} - self.callbacks = {} - - # start if neccessary - if was_running: - self.start() + debug("Can't unwatch_all() while running, please use stop()", True) + else: + debug("Unwatching all") + self.commands = {} + self.callbacks = {} def query(self, c): @@ -165,9 +150,9 @@ def run(self): # store the response self.commands[c] = r - # fire the callback, if we have one + # fire the callbacks, if there are any for callback in self.callbacks[c]: callback(r) else: - time.sleep(1) # idle until the user calls stop() or watch() + time.sleep(1) # idle From e4e230794a23389e315e02cc4c5b4511f1eaadee Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 27 Dec 2014 15:23:08 -0500 Subject: [PATCH 134/569] added module name as prefix to debug output --- obd/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/debug.py b/obd/debug.py index 51cf3c88..3bfcfb9c 100644 --- a/obd/debug.py +++ b/obd/debug.py @@ -36,7 +36,7 @@ def __init__(self): def __call__(self, msg, forcePrint=False): if self.console or forcePrint: - print(msg) + print("[obd] " + msg) if hasattr(self.handler, '__call__'): self.handler(msg) From c55d66f10a648f4bb1d731d5c1f54b4133f390ec Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 27 Dec 2014 15:39:23 -0500 Subject: [PATCH 135/569] cast any objects given for debug as strings --- obd/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/debug.py b/obd/debug.py index 3bfcfb9c..0dc5ff4f 100644 --- a/obd/debug.py +++ b/obd/debug.py @@ -36,7 +36,7 @@ def __init__(self): def __call__(self, msg, forcePrint=False): if self.console or forcePrint: - print("[obd] " + msg) + print("[obd] " + str(msg)) if hasattr(self.handler, '__call__'): self.handler(msg) From 25a949edf2fc2723d9f67ce095e61a60a38d9054 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 27 Dec 2014 16:07:01 -0500 Subject: [PATCH 136/569] added error prevention to fuel_status and air_status --- obd/decoders.py | 43 +++++++++++++++++++++++++++++++----------- tests/test_decoders.py | 10 ++++++++++ 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index ab92563a..8c97befd 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -31,6 +31,7 @@ import math from utils import * from codes import * +from debug import debug ''' All decoders take the form: @@ -261,27 +262,47 @@ def status(_hex): def fuel_status(_hex): - v = unhex(_hex[0:2]) - i = int(math.log(v, 2)) # only a single bit should be on + v = unhex(_hex[0:2]) # todo, support second fuel system - v = "Error: Unknown fuel status response" + if v <= 0: + debug("Invalid fuel status response (v <= 0)", True) + return (None, Unit.NONE) - if i < len(FUEL_STATUS): - v = FUEL_STATUS[i] + i = math.log(v, 2) # only a single bit should be on - return (v, Unit.NONE) + if i % 1 != 0: + debug("Invalid fuel status response (multiple bits set)", True) + return (None, Unit.NONE) + + i = int(i) + + if i >= len(FUEL_STATUS): + debug("Invalid fuel status response (no table entry)", True) + return (None, Unit.NONE) + + return (FUEL_STATUS[i], Unit.NONE) def air_status(_hex): v = unhex(_hex) - i = int(math.log(v, 2)) # only a single bit should be on - v = "Error: Unknown air status response" + if v <= 0: + debug("Invalid air status response (v <= 0)", True) + return (None, Unit.NONE) - if i < len(AIR_STATUS): - v = AIR_STATUS[i] + i = math.log(v, 2) # only a single bit should be on - return (v, Unit.NONE) + if i % 1 != 0: + debug("Invalid air status response (multiple bits set)", True) + return (None, Unit.NONE) + + i = int(i) + + if i >= len(AIR_STATUS): + debug("Invalid air status response (no table entry)", True) + return (None, Unit.NONE) + + return (AIR_STATUS[i], Unit.NONE) def obd_compliance(_hex): i = unhex(_hex) diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 1bf5de0a..568768e4 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -129,3 +129,13 @@ def test_distance(): def test_fuel_rate(): assert d.fuel_rate("0000") == (0.0, Unit.LPH) assert d.fuel_rate("FFFF") == (3276.75, Unit.LPH) + +def test_fuel_status(): + assert d.fuel_status("0100") == ("Open loop due to insufficient engine temperature", Unit.NONE) + assert d.fuel_status("0800") == ("Open loop due to system failure", Unit.NONE) + assert d.fuel_status("0300") == (None, Unit.NONE) + +def test_air_status(): + assert d.air_status("01") == ("Upstream", Unit.NONE) + assert d.air_status("08") == ("Pump commanded on for diagnostics", Unit.NONE) + assert d.air_status("03") == (None, Unit.NONE) From 95bda4b0cc5494109eb0eb21bc7ac0c5694941ae Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 15 Jan 2015 21:45:09 -0500 Subject: [PATCH 137/569] added better has_ checking in obd.commands --- obd/commands.py | 41 ++++++++++++++++++++++++++++++++++++----- obd/obd.py | 10 +++++----- tests/test_commands.py | 20 ++++++++++++++++++++ 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index d09e464c..061591b8 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -98,7 +98,10 @@ def __hash__(self): return hash((self.mode, self.pid)) def __eq__(self, other): - return (self.mode, self.pid) == (other.mode, other.pid) + if isinstance(other, OBDCommand): + return (self.mode, self.pid) == (other.mode, other.pid) + else: + return False ''' @@ -264,6 +267,7 @@ def __init__(self): for c in m: self.__dict__[c.name] = c + def __getitem__(self, key): if isinstance(key, int): return self.modes[key] @@ -272,12 +276,18 @@ def __getitem__(self, key): else: debug("OBD commands can only be retrieved by PID value or dict name", True) + def __len__(self): l = 0 for m in self.modes: l += len(m) return l + + def __contains__(self, s): + return self.has_name(s) + + # returns a list of PID GET commands def pid_getters(self): getters = [] @@ -287,16 +297,36 @@ def pid_getters(self): getters.append(c) return getters - # sets the boolean for + + # sets the boolean supported flag for the given command def set_supported(self, mode, pid, v): if isinstance(v, bool): - if (mode < len(self.modes)) and (pid < len(self.modes[mode])): + if self.has(mode, pid): self.modes[mode][pid].supported = v else: debug("set_supported() only accepts boolean values", True) + + # checks for existance of command by OBDCommand object + def has_command(self, c): + if isinstance(c, OBDCommand): + return c in self.__dict__.values() + else: + debug("has_command() only accepts OBDCommand objects", True) + return False + + + # checks for existance of command by name + def has_name(self, s): + if isinstance(s, basestring): + return s.isupper() and (s in self.__dict__.keys()) + else: + debug("has_name() only accepts string names for commands", True) + return False + + # checks for existance of int mode and int pid - def has(self, mode, pid): + def has_pid(self, mode, pid): if isinstance(mode, int) and isinstance(pid, int): if (mode < 0) or (pid < 0): return False @@ -306,8 +336,9 @@ def has(self, mode, pid): return False return True else: - debug("has() only accepts integer values for mode and PID", True) + debug("has_pid() only accepts integer values for mode and PID", True) return False + # export this object commands = Commands() diff --git a/obd/obd.py b/obd/obd.py index cc64907f..372c2eb4 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -106,7 +106,7 @@ def load_commands(self): for get in pid_getters: # GET commands should sequentialy turn themselves on (become marked as supported) # MODE 1 PID 0 is marked supported by default - if not self.has_command(get): + if not self.supports(get): continue response = self.send(get) # ask nicely @@ -123,7 +123,7 @@ def load_commands(self): mode = get.get_mode_int() pid = get.get_pid_int() + i + 1 - if commands.has(mode, pid): + if commands.has_pid(mode, pid): c = commands[mode][pid] c.supported = True @@ -139,8 +139,8 @@ def print_commands(self): print str(c) - def has_command(self, c): - return commands.has(c.get_mode_int(), c.get_pid_int()) and c.supported + def supports(self, c): + return commands.has_pid(c.get_mode_int(), c.get_pid_int()) and c.supported def send(self, c): @@ -161,7 +161,7 @@ def query(self, c, force=False): """ facade 'send' command, protects against sending unsupported commands """ # check that the command is supported - if not (self.has_command(c) or force): + if not (self.supports(c) or force): debug("'%s' is not supported" % str(c), True) return Response() # return empty response else: diff --git a/tests/test_commands.py b/tests/test_commands.py index 1b18b831..67d31637 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -13,6 +13,7 @@ def test_list_integrity(): # make sure all the fields are set assert cmd.name != "" + assert cmd.name.isupper() assert cmd.desc != "" assert (mode >= 1) and (mode <= 9) assert (pid >= 0) and (pid <= 196) @@ -44,6 +45,25 @@ def test_getitem(): assert cmd == obd.commands[cmd.name] +def test_contains(): + for cmds in obd.commands.modes: + for cmd in cmds: + + # by (command) + assert obd.commands.has_command(cmd) + + # by (mode, pid) + mode = cmd.get_mode_int() + pid = cmd.get_pid_int() + assert obd.commands.has_pid(mode, pid) + + # by (name) + assert obd.commands.has_name(cmd.name) + + # by `in` + assert cmd.name in obd.commands + + def test_pid_getters(): # ensure that all pid getters are found pid_getters = obd.commands.pid_getters() From 87832cf3f0408ae64173ecb10eea45988394fb86 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 15 Jan 2015 22:03:13 -0500 Subject: [PATCH 138/569] added descriptions and invalid argument testing --- tests/test_commands.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 67d31637..159bbbc4 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -12,13 +12,14 @@ def test_list_integrity(): assert pid == cmd.get_pid_int() # make sure all the fields are set - assert cmd.name != "" - assert cmd.name.isupper() - assert cmd.desc != "" - assert (mode >= 1) and (mode <= 9) - assert (pid >= 0) and (pid <= 196) - assert cmd.bytes >= 0 - assert hasattr(cmd.decode, '__call__') + assert cmd.name != "", "Command names must not be null" + assert cmd.name.isupper(), "Command names must be upper case" + assert ' ' not in cmd.name, "No spaces allowed in command names" + assert cmd.desc != "", "Command description must not be null" + assert (mode >= 1) and (mode <= 9), "Mode must be in the range [1, 9] (decimal)" + assert (pid >= 0) and (pid <= 196), "PID must be in the range [0, 196] (decimal)" + assert cmd.bytes >= 0, "Number of return bytes must be >= 0" + assert hasattr(cmd.decode, '__call__'), "Decode is not callable" def test_unique_names(): @@ -27,7 +28,7 @@ def test_unique_names(): for cmds in obd.commands.modes: for cmd in cmds: - assert not names.has_key(cmd.name) + assert not names.has_key(cmd.name), "Two commands share the same name: %s" % cmd.name names[cmd.name] = True @@ -39,13 +40,14 @@ def test_getitem(): # by [mode][pid] mode = cmd.get_mode_int() pid = cmd.get_pid_int() - assert cmd == obd.commands[mode][pid] + assert cmd == obd.commands[mode][pid], "mode %d, PID %d could not be accessed through __getitem__" % (mode, pid) # by [name] - assert cmd == obd.commands[cmd.name] + assert cmd == obd.commands[cmd.name], "command name %s could not be accessed through __getitem__" % (cmd.name) def test_contains(): + for cmds in obd.commands.modes: for cmd in cmds: @@ -63,6 +65,12 @@ def test_contains(): # by `in` assert cmd.name in obd.commands + # test things NOT in the tables, or invalid parameters + assert 'modes' not in obd.commands + assert not obd.commands.has_pid(-1, 0) + assert not obd.commands.has_pid(1, -1) + assert not obd.commands.has_command("I'm a string, not an OBDCommand") + def test_pid_getters(): # ensure that all pid getters are found From 5b676c26dbb69a93d85f123c1c5714674e7fd0aa Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 15 Jan 2015 22:47:18 -0500 Subject: [PATCH 139/569] simplified scanSerial with globbing --- obd/utils.py | 44 +++++++++++++++++------------------------- tests/test_commands.py | 4 ++-- 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/obd/utils.py b/obd/utils.py index a8d08c75..076216b4 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -32,6 +32,8 @@ import errno import string import time +import glob +import sys from debug import debug @@ -132,7 +134,7 @@ def tryPort(portStr): """returns boolean for port availability""" try: s = serial.Serial(portStr) - s.close() # explicit close 'cause of delayed GC in java + s.close() # explicit close 'cause of delayed GC in java return True except serial.SerialException: @@ -149,30 +151,20 @@ def scanSerial(): """scan for available ports. return a list of serial names""" available = [] - # Enable Bluetooh connection - for i in range(10): - portStr = "/dev/rfcomm%d" % i - if tryPort(portStr): - available.append(portStr) - - # Enable USB connection - for i in range(256): - portStr = "/dev/ttyUSB%d" % i - if tryPort(portStr): - available.append(portStr) - - #Enable for Windows - for i in range(256): - portStr = "\\.\COM%d" % i - if tryPort(portStr): - available.append(portStr) - - # Enable obdsim - ''' - for i in range(256): - portStr = "/dev/pts/%d" % i - if tryPort(portStr): - available.append(portStr) - ''' + possible_ports = [] + + if sys.platform.startswith('linux') or sys.platform.startswith('cygwin'): + possible_ports += glob.glob("/dev/rfcomm[0-9]*") + possible_ports += glob.glob("/dev/ttyUSB[0-9]*") + elif sys.platform.startswith('win'): + possible_ports += ["\\.\COM%d" % i for i in range(256)] + elif sys.platform.startswith('darwin'): + possible_ports += glob.glob('/dev/tty.*') + + # possible_ports += glob.glob('/dev/pts/[0-9]*') # for obdsim + + for port in possible_ports: + if tryPort(port): + available.append(port) return available diff --git a/tests/test_commands.py b/tests/test_commands.py index 159bbbc4..9f582e5a 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -8,8 +8,8 @@ def test_list_integrity(): for pid, cmd in enumerate(cmds): # make sure the command tables are in mode & PID order - assert mode == cmd.get_mode_int() - assert pid == cmd.get_pid_int() + assert mode == cmd.get_mode_int(), "Command is in the wrong mode list: %s" % cmd.name + assert pid == cmd.get_pid_int(), "The index in the list must also be the PID: %s" % cmd.name # make sure all the fields are set assert cmd.name != "", "Command names must not be null" From ca8af3754b990277f4a6d09e3da165e06aea9f63 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 20 Jan 2015 15:10:20 -0500 Subject: [PATCH 140/569] added error checking to port.py and turned on headers --- obd/debug.py | 2 +- obd/obd.py | 12 +++---- obd/port.py | 99 ++++++++++++++++++++++++++++++++-------------------- 3 files changed, 68 insertions(+), 45 deletions(-) diff --git a/obd/debug.py b/obd/debug.py index 0dc5ff4f..617656e0 100644 --- a/obd/debug.py +++ b/obd/debug.py @@ -30,7 +30,7 @@ class Debug(): def __init__(self): - self.console = False + self.console = True self.handler = None def __call__(self, msg, forcePrint=False): diff --git a/obd/obd.py b/obd/obd.py index 372c2eb4..de9f4201 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -43,15 +43,13 @@ def __init__(self, portstr=None): self.port = None self.supported_commands = [] - # initialize by connecting and loading sensors - self.connect(portstr) - debug("========================================") + debug("========================== Starting python-OBD ==========================") + self.connect(portstr) # initialize by connecting and loading sensors + debug("=========================================================================") def connect(self, portstr=None): """ attempts to instantiate an OBDPort object. Loads commands on success""" - - debug("Starting python-OBD") if portstr is None: debug("Using scanSerial to select port") @@ -153,8 +151,8 @@ def send(self, c): # send the query debug("Sending command: %s" % str(c)) - self.port.send(c.get_command()) # send command to the port - return c.compute(self.port.get()) # get the data, and compute a response object + r = self.port.write_and_read(c.get_command()) # send command and retrieve response + return c.compute(r) # compute a response object def query(self, c, force=False): diff --git a/obd/port.py b/obd/port.py index 1a9574a5..d1c0f37b 100644 --- a/obd/port.py +++ b/obd/port.py @@ -46,63 +46,74 @@ class OBDPort: def __init__(self, portname): """Initializes port by resetting device and gettings supported PIDs. """ - # These should really be set by the user. - baud = 38400 - databits = 8 - parity = serial.PARITY_NONE - stopbits = 1 - timeout = 2 #seconds - self.ELMver = "Unknown" self.state = State.Unconnected self.port = None - debug("Opening serial port...") + # ------------- open port ------------- + + debug("Opening serial port '%s'" % portname) try: self.port = serial.Serial(portname, \ - baud, \ - parity = parity, \ - stopbits = stopbits, \ - bytesize = databits, \ - timeout = timeout) + baudrate = 38400, \ + parity = serial.PARITY_NONE, \ + stopbits = 1, \ + bytesize = 8, \ + timeout = 2) # seconds except serial.SerialException as e: - self.error(e) + self.__error(e) return except OSError as e: - self.error(e) + self.__error(e) return debug("Serial port successfully opened on " + self.get_port_name()) + # ------------- atz (reset) ------------- try: - self.send("atz") # initialize - time.sleep(1) - self.ELMver = self.get() + r = self.write_and_read("atz", 1) # wait 1 second for ELM to initialize - if self.ELMver is None : - self.error("ELMver did not return") + if not r: + self.__error("atz (reset) did not return with an ELM version") return - debug("atz response: " + self.ELMver) + debug("atz response: " + repr(r)) + self.ELMver = r except serial.SerialException as e: - self.error(e) + self.__error(e) return - self.send("ate0") # echo off - debug("ate0 response: " + self.get()) - debug("Connected to ECU") - self.state = State.Connected + # ------------- ate0 (echo OFF) ------------- + r = self.write_and_read("ate0") + debug("ate0 response: " + repr(r)) + + if (len(r) < 2) or (r[-2:] != "OK"): + self.__error("ate0 did not return 'OK'") + return + # ------------- ath1 (headers ON) ------------- + r = self.write_and_read("ath1") + debug("ath1 response: " + repr(r)) - def error(self, msg=None): - """ called when connection error has been encountered """ + if r != 'OK': + self.__error("ath1 did not return 'OK', or echoing is still ON") + return + + # ------------- done ------------- + debug("Connection successful") + self.state = State.Connected + + + def __error(self, msg=None): + """ handles fatal failures, print debug info and closes serial """ + debug("Connection Error:", True) if msg is not None: - debug(msg, True) + debug(' ' + msg, True) if self.port is not None: self.port.close() @@ -113,22 +124,34 @@ def error(self, msg=None): def get_port_name(self): return self.port.portstr if (self.port is not None) else "No Port" + def is_connected(self): return self.state == State.Connected + def close(self): """ Resets device and closes all associated filehandles""" if (self.port != None) and (self.state == State.Connected): - self.send("atz") + self.write("atz") self.port.close() self.port = None self.ELMver = "Unknown" + def write_and_read(self, cmd, delay=None): + + self.write(cmd) + + if delay is not None: + time.sleep(delay) + + return self.read() + + # sends the hex string to the port - def send(self, cmd): + def write(self, cmd): if self.port: self.port.flushOutput() self.port.flushInput() @@ -136,8 +159,9 @@ def send(self, cmd): self.port.write(c) self.port.write("\r\n") + # accumulates and returns the ports response - def get(self): + def read(self): """Internal use only: not a public interface""" attempts = 2 @@ -153,7 +177,7 @@ def get(self): if(attempts <= 0): break - debug("get() found nothing") + debug("read() found nothing") attempts -= 1 continue @@ -172,6 +196,7 @@ def get(self): return result + # # fixme: j1979 specifies that the program should poll until the number # of returned DTCs matches the number indicated by a call to PID 01 @@ -190,8 +215,8 @@ def get_dtc(self): print "Number of stored DTC:" + str(dtcNumber) + " MIL: " + str(mil) # get all DTC, 3 per mesg response for i in range(0, ((dtcNumber+2)/3)): - self.send(GET_DTC_COMMAND) - res = self.get() + self.write(GET_DTC_COMMAND) + res = self.read() print "DTC result:" + res for i in range(0, 3): val1 = unhex(res[3+i*6:5+i*6]) @@ -205,8 +230,8 @@ def get_dtc(self): DTCCodes.append(["Active",DTCStr]) #read mode 7 - self.send(GET_FREEZE_DTC_COMMAND) - res = self.get() + self.write(GET_FREEZE_DTC_COMMAND) + res = self.read() if res[:7] == "NODATA": #no freeze frame return DTCCodes From 7f033a8f8711dadaa50f535a8cac7659e474e94e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 20 Jan 2015 17:55:07 -0500 Subject: [PATCH 141/569] improved debug information, removed syntax error, made read() and write() private --- obd/commands.py | 1 - obd/port.py | 41 +++++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 061591b8..b500e03d 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -67,7 +67,6 @@ def compute(self, _data): # create the response object with the raw data recieved r = Response(_data) - debug("command returned: %s" % _data) # strips spaces, and removes [\n\r\t] _data = "".join(_data.split()) diff --git a/obd/port.py b/obd/port.py index d1c0f37b..90bbc093 100644 --- a/obd/port.py +++ b/obd/port.py @@ -60,7 +60,7 @@ def __init__(self, portname): parity = serial.PARITY_NONE, \ stopbits = 1, \ bytesize = 8, \ - timeout = 2) # seconds + timeout = 1) # seconds except serial.SerialException as e: self.__error(e) @@ -142,58 +142,59 @@ def close(self): def write_and_read(self, cmd, delay=None): - self.write(cmd) + self.__write(cmd) if delay is not None: time.sleep(delay) - return self.read() + return self.__read() # sends the hex string to the port - def write(self, cmd): + def __write(self, cmd): if self.port: + cmd += "\r\n" # terminate self.port.flushOutput() self.port.flushInput() - for c in cmd: - self.port.write(c) - self.port.write("\r\n") + self.port.write(cmd) + debug("write: " + repr(cmd)) + else: + debug("cannot perform write() when unconnected", True) # accumulates and returns the ports response - def read(self): - """Internal use only: not a public interface""" + def __read(self): attempts = 2 result = "" - if self.port is not None: - while 1: + if self.port: + while True: c = self.port.read(1) # if nothing was recieved - if len(c) == 0: + if not c: - if(attempts <= 0): + if attempts <= 0: break debug("read() found nothing") - attempts -= 1 continue + # end on chevron + if c == ">": + break + # skip carraige returns if c == '\r': continue - # end on chevron - if c == ">": - break; - else: # whatever is left must be part of the response - result = result + c + result += c # whatever is left must be part of the response else: - debug("NO self.port!", True) + debug("cannot perform read() when unconnected", True) + debug("read: " + repr(result)) return result From 3ba75edce692f35da723302a2e2afd5aaeb7d384 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 20 Jan 2015 18:10:01 -0500 Subject: [PATCH 142/569] removed redundant debug statements --- obd/obd.py | 1 + obd/port.py | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/obd/obd.py b/obd/obd.py index de9f4201..6da9718d 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -54,6 +54,7 @@ def connect(self, portstr=None): if portstr is None: debug("Using scanSerial to select port") portnames = scanSerial() + debug("Available ports: " + str(portnames)) for port in portnames: diff --git a/obd/port.py b/obd/port.py index 90bbc093..79659e9f 100644 --- a/obd/port.py +++ b/obd/port.py @@ -74,12 +74,9 @@ def __init__(self, portname): # ------------- atz (reset) ------------- try: r = self.write_and_read("atz", 1) # wait 1 second for ELM to initialize - if not r: self.__error("atz (reset) did not return with an ELM version") return - - debug("atz response: " + repr(r)) self.ELMver = r except serial.SerialException as e: @@ -88,16 +85,12 @@ def __init__(self, portname): # ------------- ate0 (echo OFF) ------------- r = self.write_and_read("ate0") - debug("ate0 response: " + repr(r)) - if (len(r) < 2) or (r[-2:] != "OK"): self.__error("ate0 did not return 'OK'") return # ------------- ath1 (headers ON) ------------- r = self.write_and_read("ath1") - debug("ath1 response: " + repr(r)) - if r != 'OK': self.__error("ath1 did not return 'OK', or echoing is still ON") return From 490400a52d85a460d9c97fe20dd818d5ddb707d4 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 20 Jan 2015 20:29:12 -0500 Subject: [PATCH 143/569] new compute() logic, handles multiline and multi-ECU --- obd/commands.py | 54 +++++++++++++++++++++++++++++++++++-------------- obd/port.py | 4 ++-- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index b500e03d..12f0f02c 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -28,6 +28,7 @@ # # ######################################################################## +import re from decoders import * from utils import * from debug import debug @@ -62,30 +63,53 @@ def get_pid_int(self): return unhex(self.pid) def compute(self, _data): - # _data will be the string returned from the device. - # It should look something like this: '41 11 0 0\r\r' + # _data will be the string returned from the car (ELM adapter). + # It should look something like this: + # + # Mode ____Data____ + # | | | + # "\r\r48 6B 10 41 00 BE 1F B8 11 AA\r\r" + # || || + # ECU PID # create the response object with the raw data recieved r = Response(_data) - # strips spaces, and removes [\n\r\t] - _data = "".join(_data.split()) + # split by lines, and remove empty lines + lines = filter(bool, re.split("[\r\n]", _data)) - if (len(_data) > 0) and ("NODATA" not in _data) and isHex(_data): + # splits each line by spaces (each element should be a hex byte) + lines = [line.split() for line in lines] - # the first 4 chars are codes from the ELM (we don't need those) - _data = _data[4:] + # filter by minimum response length (number of space delimited chunks (bytes)) + lines = filter(lambda line: len(line) >= 6, lines) - # constrain number of bytes in response - if (self.bytes > 0): # zero bytes means flexible response - _data = constrainHex(_data, self.bytes) + # filter for ECU 10 (engine) + lines = filter(lambda line: line[2] == '10', lines) - # decoded value into the response object - r.set(self.decode(_data)) + # by now, we should have only one line. + # Any more, and its a multiline response (which this library can't handle yet) + if len(lines) == 0: + debug("no valid data returned") + elif len(lines) > 1: + debug("multiline response returned, can't handle that (yet)") + else: # len(lines) == 1 - else: - # not a parseable response - debug("return data could not be decoded") + # combine the bytes back into a hex string, excluding the header + mode + pid + _data = "".join(lines[0][5:]) + + if ("NODATA" not in _data) and isHex(_data): + + # constrain number of bytes in response + if (self.bytes > 0): # zero bytes means flexible response + _data = constrainHex(_data, self.bytes) + + # decoded value into the response object + r.set(self.decode(_data)) + + else: + # not a parseable response + debug("return data could not be decoded") return r diff --git a/obd/port.py b/obd/port.py index 79659e9f..d1601821 100644 --- a/obd/port.py +++ b/obd/port.py @@ -179,8 +179,8 @@ def __read(self): if c == ">": break - # skip carraige returns - if c == '\r': + # skip null characters (ELM spec page 9) + if c == '\x00': continue result += c # whatever is left must be part of the response From 7c569054de080e56ddc024f3a669a662f81fa079 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 20 Jan 2015 20:37:37 -0500 Subject: [PATCH 144/569] updated tests to include headers --- obd/port.py | 2 +- tests/test_OBD.py | 19 ++++++++++--------- tests/test_OBDCommand.py | 8 ++++---- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/obd/port.py b/obd/port.py index d1601821..0763ee4b 100644 --- a/obd/port.py +++ b/obd/port.py @@ -106,7 +106,7 @@ def __error(self, msg=None): debug("Connection Error:", True) if msg is not None: - debug(' ' + msg, True) + debug(' ' + str(msg), True) if self.port is not None: self.port.close() diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 6d921117..607de3d8 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -18,41 +18,42 @@ def test_query(): # forge our own command, to control the output cmd = OBDCommand("", "", "01", "23", 2, noop) - # forge IO from the car by overwriting the get/send functions + # forge IO from the car by overwriting the read/write functions # buffers toCar = [""] # needs to be inside mutable object to allow assignment in closure fromCar = "" - def send(cmd): + def write(cmd): toCar[0] = cmd o.is_connected = lambda *args: True - o.port.send = send - o.port.get = lambda *args: fromCar + o.port.port = True + o.port._OBDPort__write = write + o.port._OBDPort__read = lambda *args: fromCar - # make sure unsupported commands don't send - fromCar = "41 23 AB CD\r\r" + # make sure unsupported commands don't write + fromCar = "48 6B 10 41 23 AB CD\r\r" r = o.query(cmd) assert toCar[0] == "" assert r.is_null() # a correct command transaction - fromCar = "41 23 AB CD\r\r" # preset the response + fromCar = "48 6B 10 41 23 AB CD\r\r" # preset the response r = o.query(cmd, force=True) # run assert toCar[0] == "0123" # verify that the command was sent correctly assert r.raw_data == fromCar # verify that raw_data was stored in the Response assert r.value == "ABCD" # verify that the response was parsed correctly # response of greater length - fromCar = "41 23 AB CD EF\r\r" + fromCar = "48 6B 10 41 23 AB CD EF\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.raw_data == fromCar assert r.value == "ABCD" # response of greater length - fromCar = "41 23 AB\r\r" + fromCar = "48 6B 10 41 23 AB\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.raw_data == fromCar diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index 879f5775..9003cd2a 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -39,7 +39,7 @@ def test_clone(): def test_data_stripping(): # name description mode cmd bytes decoder cmd = OBDCommand("Test", "example OBD command", "01", "00", 2, noop) - r = cmd.compute("41 00 01 01\r\n") + r = cmd.compute("48 6B 10 41 00 01 01\r\n") assert not r.is_null() assert r.value == "0101" @@ -47,14 +47,14 @@ def test_data_stripping(): def test_data_not_hex(): # name description mode cmd bytes decoder cmd = OBDCommand("Test", "example OBD command", "01", "00", 2, noop) - r = cmd.compute("41 00 wx yz\r\n") + r = cmd.compute("48 6B 10 41 00 wx yz\r\n") assert r.is_null() def test_data_length(): # name description mode cmd bytes decoder cmd = OBDCommand("Test", "example OBD command", "01", "00", 2, noop) - r = cmd.compute("41 00 01 23 45\r\n") + r = cmd.compute("48 6B 10 41 00 01 23 45\r\n") assert r.value == "0123" - r = cmd.compute("41 00 01\r\n") + r = cmd.compute("48 6B 10 41 00 01\r\n") assert r.value == "0100" From 3c358beb4558b2a3d89614fff526f68ddf718a9e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 20 Jan 2015 20:52:31 -0500 Subject: [PATCH 145/569] added tests for multiline and multi-ECU responses --- tests/test_OBD.py | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 607de3d8..fc57a493 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -32,51 +32,73 @@ def write(cmd): o.port._OBDPort__write = write o.port._OBDPort__read = lambda *args: fromCar - # make sure unsupported commands don't write + # make sure unsupported commands don't write ------------------------------ fromCar = "48 6B 10 41 23 AB CD\r\r" r = o.query(cmd) assert toCar[0] == "" assert r.is_null() - # a correct command transaction + # a correct command transaction ------------------------------------------- fromCar = "48 6B 10 41 23 AB CD\r\r" # preset the response r = o.query(cmd, force=True) # run assert toCar[0] == "0123" # verify that the command was sent correctly assert r.raw_data == fromCar # verify that raw_data was stored in the Response assert r.value == "ABCD" # verify that the response was parsed correctly - # response of greater length + # response of greater length ---------------------------------------------- fromCar = "48 6B 10 41 23 AB CD EF\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.raw_data == fromCar assert r.value == "ABCD" - # response of greater length + # response of lesser length ----------------------------------------------- fromCar = "48 6B 10 41 23 AB\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.raw_data == fromCar assert r.value == "AB00" - # NO DATA response + # NO DATA response -------------------------------------------------------- fromCar = "NO DATA" r = o.query(cmd, force=True) assert r.raw_data == fromCar assert r.is_null() - # malformed response + # malformed response ------------------------------------------------------ fromCar = "totaly not hex!@#$" r = o.query(cmd, force=True) assert r.raw_data == fromCar assert r.is_null() - # no response + # no response ------------------------------------------------------------- fromCar = "" r = o.query(cmd, force=True) assert r.raw_data == fromCar assert r.is_null() + # disregard responses from other ECUs ------------------------------------- + fromCar = "48 6B 12 41 23 AB CD\r\r" + r = o.query(cmd, force=True) + assert toCar[0] == "0123" + assert r.raw_data == fromCar + assert r.is_null() + + # filter for ECU 10 ------------------------------------------------------- + fromCar = "48 6B 12 41 23 AB CD\r\r 48 6B 10 41 23 AB CD\r\r" + r = o.query(cmd, force=True) + assert toCar[0] == "0123" + assert r.raw_data == fromCar + assert r.value == "ABCD" + + # ignore multiline responses ---------------------------------------------- + fromCar = "48 6B 10 41 23 AB CD\r\r 48 6B 10 41 23 AB CD\r\r" + r = o.query(cmd, force=True) + assert toCar[0] == "0123" + assert r.raw_data == fromCar + assert r.is_null() + + def test_load_commands(): pass From 4c5693dda10506f52cf2b5dd79739ec49bc8b331 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 20 Jan 2015 21:20:06 -0500 Subject: [PATCH 146/569] added exclude for Mac onboard bluetooth (paired devices get their own port) --- obd/utils.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/obd/utils.py b/obd/utils.py index 076216b4..9444391f 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -130,7 +130,7 @@ def constrainHex(_hex, b): return _hex -def tryPort(portStr): +def try_port(portStr): """returns boolean for port availability""" try: s = serial.Serial(portStr) @@ -156,15 +156,21 @@ def scanSerial(): if sys.platform.startswith('linux') or sys.platform.startswith('cygwin'): possible_ports += glob.glob("/dev/rfcomm[0-9]*") possible_ports += glob.glob("/dev/ttyUSB[0-9]*") + elif sys.platform.startswith('win'): possible_ports += ["\\.\COM%d" % i for i in range(256)] + elif sys.platform.startswith('darwin'): - possible_ports += glob.glob('/dev/tty.*') + exclude = [ + '/dev/tty.Bluetooth-Incoming-Port', + '/dev/tty.Bluetooth-Modem' + ] + possible_ports += [port for port in glob.glob('/dev/tty.*') if port not in exclude] # possible_ports += glob.glob('/dev/pts/[0-9]*') # for obdsim for port in possible_ports: - if tryPort(port): + if try_port(port): available.append(port) return available From 49c6464d9362e26c1259f23a40ce95704ef7ffe2 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 20 Jan 2015 21:37:06 -0500 Subject: [PATCH 147/569] handle trailing checksum, and properly validate init commands --- obd/commands.py | 10 +++++----- obd/port.py | 6 ++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 12f0f02c..e9ec8dfe 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -66,8 +66,8 @@ def compute(self, _data): # _data will be the string returned from the car (ELM adapter). # It should look something like this: # - # Mode ____Data____ - # | | | + # Mode __Data___ + # | | | # "\r\r48 6B 10 41 00 BE 1F B8 11 AA\r\r" # || || # ECU PID @@ -82,7 +82,7 @@ def compute(self, _data): lines = [line.split() for line in lines] # filter by minimum response length (number of space delimited chunks (bytes)) - lines = filter(lambda line: len(line) >= 6, lines) + lines = filter(lambda line: len(line) >= 7, lines) # filter for ECU 10 (engine) lines = filter(lambda line: line[2] == '10', lines) @@ -95,8 +95,8 @@ def compute(self, _data): debug("multiline response returned, can't handle that (yet)") else: # len(lines) == 1 - # combine the bytes back into a hex string, excluding the header + mode + pid - _data = "".join(lines[0][5:]) + # combine the bytes back into a hex string, excluding the header + mode + pid, and trailing checksum + _data = "".join(lines[0][5:-1]) if ("NODATA" not in _data) and isHex(_data): diff --git a/obd/port.py b/obd/port.py index 0763ee4b..246a16a0 100644 --- a/obd/port.py +++ b/obd/port.py @@ -74,6 +74,7 @@ def __init__(self, portname): # ------------- atz (reset) ------------- try: r = self.write_and_read("atz", 1) # wait 1 second for ELM to initialize + r = self.__strip(r) if not r: self.__error("atz (reset) did not return with an ELM version") return @@ -85,12 +86,14 @@ def __init__(self, portname): # ------------- ate0 (echo OFF) ------------- r = self.write_and_read("ate0") + r = self.__strip(r) if (len(r) < 2) or (r[-2:] != "OK"): self.__error("ate0 did not return 'OK'") return # ------------- ath1 (headers ON) ------------- r = self.write_and_read("ath1") + r = self.__strip(r) if r != 'OK': self.__error("ath1 did not return 'OK', or echoing is still ON") return @@ -100,6 +103,9 @@ def __init__(self, portname): self.state = State.Connected + def __strip(self, s): + return "".join(s.split()) + def __error(self, msg=None): """ handles fatal failures, print debug info and closes serial """ From c55519140b742d98b7ec4bb4f98e70638d4e40fd Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 20 Jan 2015 21:51:48 -0500 Subject: [PATCH 148/569] set default console debug back to False --- obd/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/debug.py b/obd/debug.py index 617656e0..0dc5ff4f 100644 --- a/obd/debug.py +++ b/obd/debug.py @@ -30,7 +30,7 @@ class Debug(): def __init__(self): - self.console = True + self.console = False self.handler = None def __call__(self, msg, forcePrint=False): From 92f5febf303f0dfa4842615b2351c5fb461e662d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 21 Jan 2015 01:08:57 -0500 Subject: [PATCH 149/569] added checksums to the tests --- obd/port.py | 1 + tests/test_OBD.py | 14 +++++++------- tests/test_OBDCommand.py | 8 ++++---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/obd/port.py b/obd/port.py index 246a16a0..ca43491e 100644 --- a/obd/port.py +++ b/obd/port.py @@ -106,6 +106,7 @@ def __init__(self, portname): def __strip(self, s): return "".join(s.split()) + def __error(self, msg=None): """ handles fatal failures, print debug info and closes serial """ diff --git a/tests/test_OBD.py b/tests/test_OBD.py index fc57a493..bbe281ae 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -33,27 +33,27 @@ def write(cmd): o.port._OBDPort__read = lambda *args: fromCar # make sure unsupported commands don't write ------------------------------ - fromCar = "48 6B 10 41 23 AB CD\r\r" + fromCar = "48 6B 10 41 23 AB CD 10\r\r" r = o.query(cmd) assert toCar[0] == "" assert r.is_null() # a correct command transaction ------------------------------------------- - fromCar = "48 6B 10 41 23 AB CD\r\r" # preset the response + fromCar = "48 6B 10 41 23 AB CD 10\r\r" # preset the response r = o.query(cmd, force=True) # run assert toCar[0] == "0123" # verify that the command was sent correctly assert r.raw_data == fromCar # verify that raw_data was stored in the Response assert r.value == "ABCD" # verify that the response was parsed correctly # response of greater length ---------------------------------------------- - fromCar = "48 6B 10 41 23 AB CD EF\r\r" + fromCar = "48 6B 10 41 23 AB CD EF 10\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.raw_data == fromCar assert r.value == "ABCD" # response of lesser length ----------------------------------------------- - fromCar = "48 6B 10 41 23 AB\r\r" + fromCar = "48 6B 10 41 23 AB 10\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.raw_data == fromCar @@ -78,21 +78,21 @@ def write(cmd): assert r.is_null() # disregard responses from other ECUs ------------------------------------- - fromCar = "48 6B 12 41 23 AB CD\r\r" + fromCar = "48 6B 12 41 23 AB CD 10\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.raw_data == fromCar assert r.is_null() # filter for ECU 10 ------------------------------------------------------- - fromCar = "48 6B 12 41 23 AB CD\r\r 48 6B 10 41 23 AB CD\r\r" + fromCar = "48 6B 12 41 23 AB CD\r\r 48 6B 10 41 23 AB CD 10\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.raw_data == fromCar assert r.value == "ABCD" # ignore multiline responses ---------------------------------------------- - fromCar = "48 6B 10 41 23 AB CD\r\r 48 6B 10 41 23 AB CD\r\r" + fromCar = "48 6B 10 41 23 AB CD\r\r 48 6B 10 41 23 AB CD 10\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.raw_data == fromCar diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index 9003cd2a..c79ce254 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -39,7 +39,7 @@ def test_clone(): def test_data_stripping(): # name description mode cmd bytes decoder cmd = OBDCommand("Test", "example OBD command", "01", "00", 2, noop) - r = cmd.compute("48 6B 10 41 00 01 01\r\n") + r = cmd.compute("48 6B 10 41 00 01 01 10\r\n") assert not r.is_null() assert r.value == "0101" @@ -47,14 +47,14 @@ def test_data_stripping(): def test_data_not_hex(): # name description mode cmd bytes decoder cmd = OBDCommand("Test", "example OBD command", "01", "00", 2, noop) - r = cmd.compute("48 6B 10 41 00 wx yz\r\n") + r = cmd.compute("48 6B 10 41 00 wx yz 10\r\n") assert r.is_null() def test_data_length(): # name description mode cmd bytes decoder cmd = OBDCommand("Test", "example OBD command", "01", "00", 2, noop) - r = cmd.compute("48 6B 10 41 00 01 23 45\r\n") + r = cmd.compute("48 6B 10 41 00 01 23 45 10\r\n") assert r.value == "0123" - r = cmd.compute("48 6B 10 41 00 01\r\n") + r = cmd.compute("48 6B 10 41 00 01 10\r\n") assert r.value == "0100" From c1d64a3571081129ad926bec830dd25a2954f493 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 21 Jan 2015 01:10:02 -0500 Subject: [PATCH 150/569] added comment pointing to checksum --- obd/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index e9ec8dfe..7a58d40c 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -69,8 +69,8 @@ def compute(self, _data): # Mode __Data___ # | | | # "\r\r48 6B 10 41 00 BE 1F B8 11 AA\r\r" - # || || - # ECU PID + # || || || + # ECU PID Checksum # create the response object with the raw data recieved r = Response(_data) From ae7e166bfafb2c9405a9051a0295fbd7343ff33b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 21 Jan 2015 14:40:34 -0500 Subject: [PATCH 151/569] naming problem in Async, allow lone responses to be parsed regardless of ECU code --- obd/async.py | 2 +- obd/commands.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/obd/async.py b/obd/async.py index 6a0559a6..8854c033 100644 --- a/obd/async.py +++ b/obd/async.py @@ -80,7 +80,7 @@ def watch(self, c, callback=None, force=False): debug("Can't watch() while running, please use stop()", True) else: - if not (self.has_command(c) or force): + if not (self.supports(c) or force): debug("'%s' is not supported" % str(c), True) return diff --git a/obd/commands.py b/obd/commands.py index 7a58d40c..899b4e34 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -84,8 +84,9 @@ def compute(self, _data): # filter by minimum response length (number of space delimited chunks (bytes)) lines = filter(lambda line: len(line) >= 7, lines) - # filter for ECU 10 (engine) - lines = filter(lambda line: line[2] == '10', lines) + if len(lines) > 1: + # filter for ECU 10 (engine) + lines = filter(lambda line: line[2] == '10', lines) # by now, we should have only one line. # Any more, and its a multiline response (which this library can't handle yet) From 0554db1dfb22c60896cf69c02a9008b6fe47b9c2 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 21 Jan 2015 15:39:45 -0500 Subject: [PATCH 152/569] bumped to v0.3b0.0 for testing --- obd/__init__.py | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/obd/__init__.py b/obd/__init__.py index 11a3ca9e..eb2937e2 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -28,6 +28,8 @@ # # ######################################################################## +__version__ = '0.3b0.0' + from obd import OBD from commands import commands, OBDCommand from utils import scanSerial, Unit diff --git a/setup.py b/setup.py index 17b12e38..2e041bac 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="obd", - version="0.2.0", + version="0.3b0.0", description=("Serial module for handling live sensor data from a vehicle's OBD-II port"), classifiers=[ "Operating System :: POSIX :: Linux", From bfe779d3024398559c4a82098c31bed3ed233124 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 21 Jan 2015 15:52:13 -0500 Subject: [PATCH 153/569] updated tests for new lone-response logic --- tests/test_OBD.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index bbe281ae..aa0a1ddb 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -77,28 +77,34 @@ def write(cmd): assert r.raw_data == fromCar assert r.is_null() - # disregard responses from other ECUs ------------------------------------- + # accept responses from other ECUs (when single response) ------------------------------------------------------- fromCar = "48 6B 12 41 23 AB CD 10\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.raw_data == fromCar + assert r.value == "ABCD" + + # disregard responses from other ECUs (when multiple responses)------------------------------------- + fromCar = "48 6B 12 41 23 AB CD 10\r\r48 6B 12 41 23 AB CD 10\r\r" + r = o.query(cmd, force=True) + assert toCar[0] == "0123" + assert r.raw_data == fromCar assert r.is_null() # filter for ECU 10 ------------------------------------------------------- - fromCar = "48 6B 12 41 23 AB CD\r\r 48 6B 10 41 23 AB CD 10\r\r" + fromCar = "48 6B 12 41 23 AB CD 10\r\r 48 6B 10 41 23 AB CD 10\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.raw_data == fromCar assert r.value == "ABCD" # ignore multiline responses ---------------------------------------------- - fromCar = "48 6B 10 41 23 AB CD\r\r 48 6B 10 41 23 AB CD 10\r\r" + fromCar = "48 6B 10 41 23 AB CD 10\r\r 48 6B 10 41 23 AB CD 10\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.raw_data == fromCar assert r.is_null() - def test_load_commands(): pass From 5bbf2a21b05189c13317662afe66ac6979b52a11 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 7 Feb 2015 11:47:17 -0500 Subject: [PATCH 154/569] started tweaking get_DTC functions --- obd/commands.py | 2 +- obd/decoders.py | 8 +++----- obd/obd.py | 13 +++++-------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 899b4e34..c871cfbd 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -252,7 +252,7 @@ def __eq__(self, other): __mode3__ = [ # sensor name description mode cmd bytes decoder - OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 0, noop ), + OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 6, noop ), ] __mode4__ = [ diff --git a/obd/decoders.py b/obd/decoders.py index 8c97befd..a9e01360 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -355,13 +355,11 @@ def dtc(_hex): # converts a frame of 2-byte DTCs into a list of DTCs def dtc_frame(_hex): - code_length = 4 # number of hex chars consumed by one code - size = len(_hex) / code_length # number of codes defined in THIS FRAME (not total) codes = [] - for n in range(size): + for n in range(3): - start = code_length * n - end = start + code_length + start = 4 * n + end = start + 4 codes.append(dtc(_hex[start:end])) diff --git a/obd/obd.py b/obd/obd.py index 6da9718d..c1b36cd1 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -167,20 +167,17 @@ def query(self, c, force=False): return self.send(c) - ''' def query_DTC(self): """ read all DTCs """ n = self.query(commands.STATUS).value['DTC Count']; + n = n if (n < 128) else 0 # if this number is over 128, it's invalid codes = []; - # poll until the number of commands received equals that returned from STATUS - # or until this has looped 128 times (the max number of DTCs that STATUS reports) - i = 0 - while (len(codes) < n) and (i < 128): - codes += self.query(commands.GET_DTC).value - i += 1 + while n > 0: + current_codes = self.query(commands.GET_DTC).value + codes += current_codes + n -= len(current_codes) return codes - ''' From d77b00f77ae39fb6cc663549b9f201798370545b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 7 Feb 2015 12:32:31 -0500 Subject: [PATCH 155/569] moved OBDCommand class to new file --- obd/__init__.py | 2 +- obd/command.py | 98 +++++++++++++++++++++++++++++++++++++++++++++++++ obd/commands.py | 95 +---------------------------------------------- 3 files changed, 100 insertions(+), 95 deletions(-) create mode 100644 obd/command.py diff --git a/obd/__init__.py b/obd/__init__.py index eb2937e2..91b0df6a 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -34,4 +34,4 @@ from commands import commands, OBDCommand from utils import scanSerial, Unit from debug import debug -from async import Async \ No newline at end of file +from async import Async diff --git a/obd/command.py b/obd/command.py new file mode 100644 index 00000000..9a34110e --- /dev/null +++ b/obd/command.py @@ -0,0 +1,98 @@ + +import re +from utils import * +from debug import debug + + + +class OBDCommand(): + def __init__(self, name, desc, mode, pid, returnBytes, decoder, supported=False): + self.name = name + self.desc = desc + self.mode = mode + self.pid = pid + self.bytes = returnBytes # number of bytes expected in return + self.decode = decoder + self.supported = supported + + def clone(self): + return OBDCommand(self.name, + self.desc, + self.mode, + self.pid, + self.bytes, + self.decode) + + def get_command(self): + return self.mode + self.pid # the actual command transmitted to the port + + def get_mode_int(self): + return unhex(self.mode) + + def get_pid_int(self): + return unhex(self.pid) + + def compute(self, _data): + # _data will be the string returned from the car (ELM adapter). + # It should look something like this: + # + # Mode __Data___ + # | | | + # "\r\r48 6B 10 41 00 BE 1F B8 11 AA\r\r" + # || || || + # ECU PID Checksum + + # create the response object with the raw data recieved + r = Response(_data) + + # split by lines, and remove empty lines + lines = filter(bool, re.split("[\r\n]", _data)) + + # splits each line by spaces (each element should be a hex byte) + lines = [line.split() for line in lines] + + # filter by minimum response length (number of space delimited chunks (bytes)) + lines = filter(lambda line: len(line) >= 7, lines) + + if len(lines) > 1: + # filter for ECU 10 (engine) + lines = filter(lambda line: line[2] == '10', lines) + + # by now, we should have only one line. + # Any more, and its a multiline response (which this library can't handle yet) + if len(lines) == 0: + debug("no valid data returned") + elif len(lines) > 1: + debug("multiline response returned, can't handle that (yet)") + else: # len(lines) == 1 + + # combine the bytes back into a hex string, excluding the header + mode + pid, and trailing checksum + _data = "".join(lines[0][5:-1]) + + if ("NODATA" not in _data) and isHex(_data): + + # constrain number of bytes in response + if (self.bytes > 0): # zero bytes means flexible response + _data = constrainHex(_data, self.bytes) + + # decoded value into the response object + r.set(self.decode(_data)) + + else: + # not a parseable response + debug("return data could not be decoded") + + return r + + def __str__(self): + return "%s%s: %s" % (self.mode, self.pid, self.desc) + + def __hash__(self): + # needed for using commands as keys in a dict (see async.py) + return hash((self.mode, self.pid)) + + def __eq__(self, other): + if isinstance(other, OBDCommand): + return (self.mode, self.pid) == (other.mode, other.pid) + else: + return False diff --git a/obd/commands.py b/obd/commands.py index 899b4e34..7615361f 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -28,105 +28,12 @@ # # ######################################################################## -import re +from command import OBDCommand from decoders import * -from utils import * from debug import debug -class OBDCommand(): - def __init__(self, name, desc, mode, pid, returnBytes, decoder, supported=False): - self.name = name - self.desc = desc - self.mode = mode - self.pid = pid - self.bytes = returnBytes # number of bytes expected in return - self.decode = decoder - self.supported = supported - - def clone(self): - return OBDCommand(self.name, - self.desc, - self.mode, - self.pid, - self.bytes, - self.decode) - - def get_command(self): - return self.mode + self.pid # the actual command transmitted to the port - - def get_mode_int(self): - return unhex(self.mode) - - def get_pid_int(self): - return unhex(self.pid) - - def compute(self, _data): - # _data will be the string returned from the car (ELM adapter). - # It should look something like this: - # - # Mode __Data___ - # | | | - # "\r\r48 6B 10 41 00 BE 1F B8 11 AA\r\r" - # || || || - # ECU PID Checksum - - # create the response object with the raw data recieved - r = Response(_data) - - # split by lines, and remove empty lines - lines = filter(bool, re.split("[\r\n]", _data)) - - # splits each line by spaces (each element should be a hex byte) - lines = [line.split() for line in lines] - - # filter by minimum response length (number of space delimited chunks (bytes)) - lines = filter(lambda line: len(line) >= 7, lines) - - if len(lines) > 1: - # filter for ECU 10 (engine) - lines = filter(lambda line: line[2] == '10', lines) - - # by now, we should have only one line. - # Any more, and its a multiline response (which this library can't handle yet) - if len(lines) == 0: - debug("no valid data returned") - elif len(lines) > 1: - debug("multiline response returned, can't handle that (yet)") - else: # len(lines) == 1 - - # combine the bytes back into a hex string, excluding the header + mode + pid, and trailing checksum - _data = "".join(lines[0][5:-1]) - - if ("NODATA" not in _data) and isHex(_data): - - # constrain number of bytes in response - if (self.bytes > 0): # zero bytes means flexible response - _data = constrainHex(_data, self.bytes) - - # decoded value into the response object - r.set(self.decode(_data)) - - else: - # not a parseable response - debug("return data could not be decoded") - - return r - - def __str__(self): - return "%s%s: %s" % (self.mode, self.pid, self.desc) - - def __hash__(self): - # needed for using commands as keys in a dict (see async.py) - return hash((self.mode, self.pid)) - - def __eq__(self, other): - if isinstance(other, OBDCommand): - return (self.mode, self.pid) == (other.mode, other.pid) - else: - return False - ''' Define command tables From 92fb9e787ad5b31e875a2a80f878c362020895d9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 7 Feb 2015 16:47:24 -0500 Subject: [PATCH 156/569] renamed OBDCommand file, made use of has_command() in supports() --- obd/{command.py => OBDCommand.py} | 10 +++++----- obd/__init__.py | 3 ++- obd/obd.py | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) rename obd/{command.py => OBDCommand.py} (94%) diff --git a/obd/command.py b/obd/OBDCommand.py similarity index 94% rename from obd/command.py rename to obd/OBDCommand.py index 9a34110e..79f15509 100644 --- a/obd/command.py +++ b/obd/OBDCommand.py @@ -17,11 +17,11 @@ def __init__(self, name, desc, mode, pid, returnBytes, decoder, supported=False) def clone(self): return OBDCommand(self.name, - self.desc, - self.mode, - self.pid, - self.bytes, - self.decode) + self.desc, + self.mode, + self.pid, + self.bytes, + self.decode) def get_command(self): return self.mode + self.pid # the actual command transmitted to the port diff --git a/obd/__init__.py b/obd/__init__.py index 91b0df6a..359c4a55 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -31,7 +31,8 @@ __version__ = '0.3b0.0' from obd import OBD -from commands import commands, OBDCommand +from command import OBDCommand +from commands import commands from utils import scanSerial, Unit from debug import debug from async import Async diff --git a/obd/obd.py b/obd/obd.py index 6da9718d..a0663811 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -139,7 +139,7 @@ def print_commands(self): def supports(self, c): - return commands.has_pid(c.get_mode_int(), c.get_pid_int()) and c.supported + return commands.has_command(c) and c.supported def send(self, c): From adc13b485444a90e79256cc855b63a6f1d608c87 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 7 Feb 2015 17:02:16 -0500 Subject: [PATCH 157/569] converted README back to markdown --- README.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.rst | 72 --------------------------------------------------- 2 files changed, 75 insertions(+), 72 deletions(-) create mode 100644 README.md delete mode 100644 README.rst diff --git a/README.md b/README.md new file mode 100644 index 00000000..cfc77ad0 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +python-OBD +========== + +A python module for handling realtime sensor data from OBD-II vehicle +ports. Works with ELM327 OBD-II adapters, and is fit for the Raspberry +Pi. + +Installation +------------ + +```Shell +$ pip install obd +``` + +Basic Usage +----------- + +```Python +import obd + +connection = obd.OBD() # auto-connects to USB or RF port + +cmd = obd.commands.RPM # select an OBD command (sensor) + +response = connection.query(cmd) # send the command, and parse the response + +print(response.value) +print(response.unit) +``` + +Documentation +------------- + +[Visit the GitHub Wiki!](http://github.com/brendanwhitfield/python-OBD/wiki) + +Commands +-------- + +Here are a handful of the supported commands (sensors): + +*note: support for these commands will vary from car to car* + +- Calculated Engine Load +- Engine Coolant Temperature +- Fuel Pressure +- Intake Manifold Pressure +- Engine RPM +- Vehicle Speed +- Timing Advance +- Intake Air Temp +- Air Flow Rate (MAF) +- Throttle Position +- Engine Run Time +- Fuel Level Input +- Number of warm-ups since codes cleared +- Barometric Pressure +- Ambient air temperature +- Commanded throttle actuator +- Time run with MIL on +- Time since trouble codes cleared +- Hybrid battery pack remaining life +- Engine fuel rate +- etc... For a full list, see [the wiki](http://github.com/brendanwhitfield/python-OBD/wiki) + +License +------- + +GNU GPL v2 + +This library is forked from: + +- +- + +Enjoy and drive safe! diff --git a/README.rst b/README.rst deleted file mode 100644 index 0529bd41..00000000 --- a/README.rst +++ /dev/null @@ -1,72 +0,0 @@ -python-OBD -========== - -A python module for handling realtime sensor data from OBD-II vehicle ports. Works with ELM327 OBD-II adapters, and is fit for the Raspberry Pi. - - -Installation ------------- - -:: - - $ pip install obd - - -Basic Usage ------------ - -:: - - import obd - - connection = obd.OBD() # auto-connects to USB or RF port - - cmd = obd.commands.RPM # select an OBD command (sensor) - - response = connection.query(cmd) # send the command, and parse the response - - print(response.value) - print(response.unit) - - -Documentation -------------- -`Visit the GitHub Wiki! `_ - - -Commands --------- -Here are a few of the currently supported commands (note: support for these commands will vary from car to car): - -+ Calculated Engine Load -+ Engine Coolant Temperature -+ Fuel Pressure -+ Intake Manifold Pressure -+ Engine RPM -+ Vehicle Speed -+ Timing Advance -+ Intake Air Temp -+ Air Flow Rate (MAF) -+ Throttle Position -+ Engine Run Time -+ Fuel Level Input -+ Number of warm-ups since codes cleared -+ Barometric Pressure -+ Ambient air temperature -+ Commanded throttle actuator -+ Time run with MIL on -+ Time since trouble codes cleared -+ Hybrid battery pack remaining life -+ Engine fuel rate -+ etc... (for a full list, see `the wiki `_) - -License -------- -GNU GPL v2 - -This library is forked from: - -+ https://github.com/peterh/pyobd -+ https://github.com/Pbartek/pyobd-pi - -Enjoy and drive safe! From db0ce3e03596fa0495314df695af21c12e88a178 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 7 Feb 2015 17:04:23 -0500 Subject: [PATCH 158/569] updated readme link to point to command tables --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index cfc77ad0..76853cb4 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Documentation Commands -------- -Here are a handful of the supported commands (sensors): +Here are a handful of the supported commands (sensors). For a full list, see [the wiki](https://github.com/brendanwhitfield/python-OBD/wiki/Command-Tables) *note: support for these commands will vary from car to car* @@ -60,7 +60,6 @@ Here are a handful of the supported commands (sensors): - Time since trouble codes cleared - Hybrid battery pack remaining life - Engine fuel rate -- etc... For a full list, see [the wiki](http://github.com/brendanwhitfield/python-OBD/wiki) License ------- From 5be5c571f6d2dc1929120251c762c4316bade618 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 7 Feb 2015 17:29:18 -0500 Subject: [PATCH 159/569] fixed import filenames --- obd/__init__.py | 2 +- obd/commands.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/__init__.py b/obd/__init__.py index 359c4a55..b741e8a2 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -31,7 +31,7 @@ __version__ = '0.3b0.0' from obd import OBD -from command import OBDCommand +from OBDCommand import OBDCommand from commands import commands from utils import scanSerial, Unit from debug import debug diff --git a/obd/commands.py b/obd/commands.py index 7615361f..1cdfb229 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -28,7 +28,7 @@ # # ######################################################################## -from command import OBDCommand +from OBDCommand import OBDCommand from decoders import * from debug import debug From 2835dd9bb6355c6b9b73375d6168c9b58d21b8a8 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 7 Feb 2015 17:47:18 -0500 Subject: [PATCH 160/569] bump to version 0.3.0 --- obd/__init__.py | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/obd/__init__.py b/obd/__init__.py index b741e8a2..518a5f21 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -28,7 +28,7 @@ # # ######################################################################## -__version__ = '0.3b0.0' +__version__ = '0.3.0' from obd import OBD from OBDCommand import OBDCommand diff --git a/setup.py b/setup.py index 2e041bac..02bc1009 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="obd", - version="0.3b0.0", + version="0.3.0", description=("Serial module for handling live sensor data from a vehicle's OBD-II port"), classifiers=[ "Operating System :: POSIX :: Linux", @@ -25,4 +25,4 @@ include_package_data=True, zip_safe=False, install_requires=["pyserial"], -) \ No newline at end of file +) From 6b8ff23f0498232521ecde940fd389263bd2af7e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 8 Feb 2015 16:42:19 -0500 Subject: [PATCH 161/569] started outlining structure for protocol processors --- obd/protocols/README.md | 18 ++++++++++ obd/protocols/__init__.py | 34 ++++++++++++++++++ obd/protocols/frame.py | 11 ++++++ obd/protocols/proto_names.txt | 34 ++++++++++++++++++ obd/protocols/protocol.py | 21 +++++++++++ obd/protocols/protocol_can.py | 52 +++++++++++++++++++++++++++ obd/protocols/protocol_legacy.py | 62 ++++++++++++++++++++++++++++++++ 7 files changed, 232 insertions(+) create mode 100644 obd/protocols/README.md create mode 100644 obd/protocols/__init__.py create mode 100644 obd/protocols/frame.py create mode 100644 obd/protocols/proto_names.txt create mode 100644 obd/protocols/protocol.py create mode 100644 obd/protocols/protocol_can.py create mode 100644 obd/protocols/protocol_legacy.py diff --git a/obd/protocols/README.md b/obd/protocols/README.md new file mode 100644 index 00000000..c69ef33d --- /dev/null +++ b/obd/protocols/README.md @@ -0,0 +1,18 @@ + +Inheritance structure + +``` +Protocol + LegacyProtocol + SAE_J1850_PWM + SAE_J1850_VPM + ISO_9141_2 + ISO_14230_4_5baud + ISO_14230_4_fast + CANProtocol + ISO_15765_4_11bit_500k + ISO_15765_4_29bit_500k + ISO_15765_4_11bit_250k + ISO_15765_4_29bit_250k + SAE_J1939 +``` diff --git a/obd/protocols/__init__.py b/obd/protocols/__init__.py new file mode 100644 index 00000000..a2246f60 --- /dev/null +++ b/obd/protocols/__init__.py @@ -0,0 +1,34 @@ + +from protocol_legacy import SAE_J1850_PWM, \ + SAE_J1850_VPW, \ + ISO_9141_2, \ + ISO_14230_4_5baud, \ + ISO_14230_4_fast + +from protocol_can import ISO_15765_4_11bit_500k, \ + ISO_15765_4_29bit_500k, \ + ISO_15765_4_11bit_250k, \ + ISO_15765_4_29bit_250k, \ + SAE_J1939 + + +# allow each class to be access by ELM name (the result of an "AT DP\r") +protocols = { + "SAE J1850 PWM" : SAE_J1850_PWM, + "SAE J1850 VPW" : SAE_J1850_VPW, + "ISO 9141-2" : ISO_9141_2, + "ISO 14230-4 (KWP 5BAUD)" : ISO_14230_4_5baud, + "ISO 14230-4 (KWP FAST)" : ISO_14230_4_fast, + "ISO 15765-4 (CAN 11/500)" : ISO_15765_4_11bit_500k, + "ISO 15765-4 (CAN 29/500)" : ISO_15765_4_29bit_500k, + "ISO 15765-4 (CAN 11/250)" : ISO_15765_4_11bit_250k, + "ISO 15765-4 (CAN 29/250)" : ISO_15765_4_29bit_250k, + "SAE J1939" : SAE_J1939 +} + + +def get(name): + if name in protocols: + return protocols[name] + else: + return None diff --git a/obd/protocols/frame.py b/obd/protocols/frame.py new file mode 100644 index 00000000..b547c575 --- /dev/null +++ b/obd/protocols/frame.py @@ -0,0 +1,11 @@ + + +class Frame(object): + def __init__(self, protocol, raw_bytes): + self.protocol = protocol + self.raw_bytes = raw_bytes + + self.data_bytes = [] + self.priority = None + self.rx_id = None + self.tx_id = None diff --git a/obd/protocols/proto_names.txt b/obd/protocols/proto_names.txt new file mode 100644 index 00000000..16fb4a64 --- /dev/null +++ b/obd/protocols/proto_names.txt @@ -0,0 +1,34 @@ + +AT SP 1 +'SAE J1850 PWM\r\r' + +AT SP 2 +'SAE J1850 VPW\r\r' + +AT SP 3 +'ISO 9141-2\r\r' + +AT SP 4 +'ISO 14230-4 (KWP 5BAUD)\r\r' + +AT SP 5 +'ISO 14230-4 (KWP FAST)\r\r' + +AT SP 6 +'ISO 15765-4 (CAN 11/500)\r\r' + +AT SP 7 +'ISO 15765-4 (CAN 29/500)\r\r' + +AT SP 8 +'ISO 15765-4 (CAN 11/250)\r\r' + +AT SP 9 +'ISO 15765-4 (CAN 29/250)\r\r' + +AT SP A +'SAE J1939 (CAN 29/250)\r\r' + +AT SP B +'USER1 (CAN 11/125)\r\r' + diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py new file mode 100644 index 00000000..d24dc5a9 --- /dev/null +++ b/obd/protocols/protocol.py @@ -0,0 +1,21 @@ + +""" + +Protocol objects are factories for Frames. +They are __called__ with a byte array, and return the parsed frame objects. +They are stateless + +""" + + +class Protocol(object): + def __init__(self, baud=38400): + self.baud = baud + + def create_frame(self, raw_bytes): + """ override in subclass for each protocol """ + raise NotImplementedError() + + def parse_frames(self, frames): + """ override in subclass for each protocol """ + raise NotImplementedError() diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py new file mode 100644 index 00000000..ab8d3167 --- /dev/null +++ b/obd/protocols/protocol_can.py @@ -0,0 +1,52 @@ + + +from protocol import Protocol +from frame import Frame + + +class CANProtocol(Protocol): + def __init__(self, baud, id_bits): + Protocol.__init__(baud) + self.id_bits = id_bits + + + def create_frame(self, raw_bytes): + frame = Frame(self, raw_bytes) + return frame + + + def parse_frames(self, frames): + pass + + + +############################################## +# # +# Here lie the class stubs for each protocol # +# # +############################################## + + +class ISO_15765_4_11bit_500k(CANProtocol): + def __init__(self): + CANProtocol.__init__(baud=500000, id_bits=11) + + +class ISO_15765_4_29bit_500k(CANProtocol): + def __init__(self): + CANProtocol.__init__(baud=500000, id_bits=29) + + +class ISO_15765_4_11bit_250k(CANProtocol): + def __init__(self): + CANProtocol.__init__(baud=250000, id_bits=11) + + +class ISO_15765_4_29bit_250k(CANProtocol): + def __init__(self): + CANProtocol.__init__(baud=250000, id_bits=29) + + +class SAE_J1939(CANProtocol): + def __init__(self): + CANProtocol.__init__(baud=250000, id_bits=29) diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py new file mode 100644 index 00000000..70d0b37a --- /dev/null +++ b/obd/protocols/protocol_legacy.py @@ -0,0 +1,62 @@ + + +from protocol import Protocol +from frame import Frame + + +class LegacyProtocol(Protocol): + def __init__(self, baud): + Protocol.__init__(baud) + + def create_frame(self, raw_bytes): + + frame = Frame(self, raw_bytes) + + frame.data_bytes = raw_bytes[3:-1] # exclude trailing checksum (handled by ELM adapter) + + # read header information + frame.priority = raw_bytes[0] + frame.rx_id = raw_bytes[1] + frame.tx_id = raw_bytes[2] + + return frame + + def parse_frames(self, frames): + pass + + + +############################################## +# # +# Here lie the class stubs for each protocol # +# # +############################################## + + + +class SAE_J1850_PWM(LegacyProtocol): + + def __init__(self): + LegacyProtocol.__init__(baud=41600) + + +class SAE_J1850_VPW(LegacyProtocol): + + def __init__(self): + LegacyProtocol.__init__(baud=10400) + + +class ISO_9141_2(LegacyProtocol): + + def __init__(self): + LegacyProtocol.__init__(baud=10400) + + +class ISO_14230_4_5baud(LegacyProtocol): + def __init__(self): + LegacyProtocol.__init__(baud=10400) + + +class ISO_14230_4_fast(LegacyProtocol): + def __init__(self): + LegacyProtocol.__init__(baud=10400) From d494c30e52620d1733ef45b1c8157d2fed3b1918 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 8 Feb 2015 20:46:28 -0500 Subject: [PATCH 162/569] protocols now override parse_frame and parse_message functions --- obd/protocols/frame.py | 7 ++- obd/protocols/message.py | 7 +++ obd/protocols/protocol.py | 62 +++++++++++++++++++++----- obd/protocols/protocol_can.py | 74 +++++++++++++++++++++++--------- obd/protocols/protocol_legacy.py | 32 +++++++------- obd/utils.py | 5 +++ 6 files changed, 136 insertions(+), 51 deletions(-) create mode 100644 obd/protocols/message.py diff --git a/obd/protocols/frame.py b/obd/protocols/frame.py index b547c575..0905e500 100644 --- a/obd/protocols/frame.py +++ b/obd/protocols/frame.py @@ -1,11 +1,10 @@ class Frame(object): - def __init__(self, protocol, raw_bytes): - self.protocol = protocol - self.raw_bytes = raw_bytes - + def __init__(self, raw): + self.raw = raw self.data_bytes = [] self.priority = None + self.addr_mode = None self.rx_id = None self.tx_id = None diff --git a/obd/protocols/message.py b/obd/protocols/message.py new file mode 100644 index 00000000..5d92eb00 --- /dev/null +++ b/obd/protocols/message.py @@ -0,0 +1,7 @@ + + +class Message(object): + def __init__(self, frames, tx_id): + self.frames = frames + self.tx_id = tx_id + self.data_bytes = [] diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index d24dc5a9..c1da6254 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -1,21 +1,61 @@ """ -Protocol objects are factories for Frames. -They are __called__ with a byte array, and return the parsed frame objects. -They are stateless +Protocol objects are stateless factories for Frames and Messages. +They are __called__ with the raw string response, and +return a list of Messages. """ +from frame import Frame +from message import Message +import re + + class Protocol(object): - def __init__(self, baud=38400): - self.baud = baud + def __init__(self, baud=38400): + self.baud = baud + + + def __call__(self, raw): + + # split by lines into frames, and remove empty lines + lines = filter(bool, re.split("[\r\n]", raw)) + + # ditch spaces + lines = [line.replace(' ', '') for line in lines] + + # create frame objects for each line + frames = [Frame(line) for line in lines] + + # subclass function to load the frame parameters + for frame in frames: + self.parse_frame(frame) + + # group frames by transmitting ECU (tx_id) + ecus = {} + for frame in frames: + if frame.tx_id not in ecus: + ecus[frame.tx_id] = [frame] + else: + ecus[frame.tx_id].append(frame) + + messages = [] + + for ecu in ecus: + message = Message(ecus[ecu], ecu) + # subclass function to assemble frames into data_bytes + self.parse_message(message) + messages.append(message) + + return messages + - def create_frame(self, raw_bytes): - """ override in subclass for each protocol """ - raise NotImplementedError() + def parse_frame(self, frame): + """ override in subclass for each protocol """ + raise NotImplementedError() - def parse_frames(self, frames): - """ override in subclass for each protocol """ - raise NotImplementedError() + def parse_message(self, message): + """ override in subclass for each protocol """ + raise NotImplementedError() diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index ab8d3167..ed549a28 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -2,20 +2,51 @@ from protocol import Protocol from frame import Frame +from obd.utils import ascii_to_bytes class CANProtocol(Protocol): - def __init__(self, baud, id_bits): - Protocol.__init__(baud) - self.id_bits = id_bits - - - def create_frame(self, raw_bytes): - frame = Frame(self, raw_bytes) - return frame - - - def parse_frames(self, frames): + def __init__(self, baud, id_bits): + Protocol.__init__(self, baud) + self.id_bits = id_bits + + + def parse_frame(self, frame): + + # pad 11-bit CAN headers out to 32 bits for consistency, + # since ELM already does this for 29-bit CAN headers + if self.id_bits == 11: + frame.raw = "00000" + frame.raw + + raw_bytes = ascii_to_bytes(frame.raw) + + # read header information + if self.id_bits == 11: + frame.priority = raw_bytes[2] & 0x0F # always 7 + frame.addr_mode = raw_bytes[3] & 0xF0 # 0xD0 = functional, 0xE0 = physical + + if frame.addr_mode == 0xD0: + #untested("11-bit functional request from tester") + frame.rx_id = raw_bytes[3] & 0x0F # usually (always?) 0x0F for broadcast + frame.tx_id = 0xF1 # made-up to mimic all other protocols + elif raw_bytes[3] & 0x08: + frame.rx_id = 0xF1 # made-up to mimic all other protocols + frame.tx_id = raw_bytes[3] & 0x07 + else: + #untested("11-bit message header from tester (functional or physical)") + frame.tx_id = 0xF1 # made-up to mimic all other protocols + frame.rx_id = raw_bytes[3] & 0x07 + + else: # self.id_bits == 29: + frame.priority = raw_bytes[0] # usually (always?) 0x18 + frame.addr_mode = raw_bytes[1] # DB = functional, DA = physical + frame.rx_id = raw_bytes[2] # 0x33 = broadcast (functional) + frame.tx_id = raw_bytes[3] # 0xF1 = tester ID + + frame.data_bytes = raw_bytes[5:] + + + def parse_message(self, message): pass @@ -27,26 +58,27 @@ def parse_frames(self, frames): ############################################## + class ISO_15765_4_11bit_500k(CANProtocol): - def __init__(self): - CANProtocol.__init__(baud=500000, id_bits=11) + def __init__(self): + CANProtocol.__init__(self, baud=500000, id_bits=11) class ISO_15765_4_29bit_500k(CANProtocol): - def __init__(self): - CANProtocol.__init__(baud=500000, id_bits=29) + def __init__(self): + CANProtocol.__init__(self, baud=500000, id_bits=29) class ISO_15765_4_11bit_250k(CANProtocol): - def __init__(self): - CANProtocol.__init__(baud=250000, id_bits=11) + def __init__(self): + CANProtocol.__init__(self, baud=250000, id_bits=11) class ISO_15765_4_29bit_250k(CANProtocol): - def __init__(self): - CANProtocol.__init__(baud=250000, id_bits=29) + def __init__(self): + CANProtocol.__init__(self, baud=250000, id_bits=29) class SAE_J1939(CANProtocol): - def __init__(self): - CANProtocol.__init__(baud=250000, id_bits=29) + def __init__(self): + CANProtocol.__init__(self, baud=250000, id_bits=29) diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 70d0b37a..33942a58 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -2,15 +2,17 @@ from protocol import Protocol from frame import Frame +from obd.utils import ascii_to_bytes +from obd.debug import debug class LegacyProtocol(Protocol): - def __init__(self, baud): - Protocol.__init__(baud) - def create_frame(self, raw_bytes): + def __init__(self, baud): + Protocol.__init__(self, baud) - frame = Frame(self, raw_bytes) + def parse_frame(self, frame): + raw_bytes = ascii_to_bytes(frame.raw) frame.data_bytes = raw_bytes[3:-1] # exclude trailing checksum (handled by ELM adapter) @@ -19,10 +21,13 @@ def create_frame(self, raw_bytes): frame.rx_id = raw_bytes[1] frame.tx_id = raw_bytes[2] - return frame + def parse_message(self, message): + if len(message.frames) == 1: + message.data_bytes = message.frames[0].data_bytes + else: + debug("Recieved multi-frame response. Can't parse those yet") + - def parse_frames(self, frames): - pass @@ -35,28 +40,25 @@ def parse_frames(self, frames): class SAE_J1850_PWM(LegacyProtocol): - def __init__(self): - LegacyProtocol.__init__(baud=41600) + LegacyProtocol.__init__(self, baud=41600) class SAE_J1850_VPW(LegacyProtocol): - def __init__(self): - LegacyProtocol.__init__(baud=10400) + LegacyProtocol.__init__(self, baud=10400) class ISO_9141_2(LegacyProtocol): - def __init__(self): - LegacyProtocol.__init__(baud=10400) + LegacyProtocol.__init__(self, baud=10400) class ISO_14230_4_5baud(LegacyProtocol): def __init__(self): - LegacyProtocol.__init__(baud=10400) + LegacyProtocol.__init__(self, baud=10400) class ISO_14230_4_fast(LegacyProtocol): def __init__(self): - LegacyProtocol.__init__(baud=10400) + LegacyProtocol.__init__(self, baud=10400) diff --git a/obd/utils.py b/obd/utils.py index 9444391f..c225a5cf 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -90,6 +90,11 @@ def __str__(self): return "Test %s: %s, %s" % (name, a, c) +def ascii_to_bytes(a): + b = [] + for i in range(0, len(a), 2): + b.append(int(a[i:i+2], 16)) + return b def unhex(_hex): _hex = "0" if _hex == "" else _hex From ec67f402c357d8e4ac7d07e693e626b408862b60 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 8 Feb 2015 21:01:49 -0500 Subject: [PATCH 163/569] successful parsing of single frame responses --- obd/protocols/protocol_can.py | 16 +++++++++------- obd/protocols/protocol_legacy.py | 1 - 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index ed549a28..80e3fd0c 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -1,22 +1,22 @@ from protocol import Protocol -from frame import Frame from obd.utils import ascii_to_bytes +from obd.debug import debug class CANProtocol(Protocol): + def __init__(self, baud, id_bits): Protocol.__init__(self, baud) self.id_bits = id_bits - def parse_frame(self, frame): - # pad 11-bit CAN headers out to 32 bits for consistency, + # pad 11-bit CAN headers out to 32 bits for consistency, # since ELM already does this for 29-bit CAN headers if self.id_bits == 11: - frame.raw = "00000" + frame.raw + frame.raw = "00000" + frame.raw raw_bytes = ascii_to_bytes(frame.raw) @@ -44,11 +44,13 @@ def parse_frame(self, frame): frame.tx_id = raw_bytes[3] # 0xF1 = tester ID frame.data_bytes = raw_bytes[5:] - - def parse_message(self, message): - pass + def parse_message(self, message): + if len(message.frames) == 1: + message.data_bytes = message.frames[0].data_bytes + else: + debug("Recieved multi-frame response. Can't parse those yet") ############################################## diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 33942a58..62120757 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -1,7 +1,6 @@ from protocol import Protocol -from frame import Frame from obd.utils import ascii_to_bytes from obd.debug import debug From 0ce70e41bee5cb18aa8f12aa585db8726b7101be Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 9 Feb 2015 01:25:15 -0500 Subject: [PATCH 164/569] began wiring in protocol system --- obd/OBDCommand.py | 59 ++++++----------- obd/obd.py | 15 +++-- obd/port.py | 110 ++++++++++++++++++++----------- obd/protocols/message.py | 3 +- obd/protocols/protocol.py | 7 +- obd/protocols/protocol_can.py | 2 + obd/protocols/protocol_legacy.py | 2 + obd/utils.py | 3 + 8 files changed, 112 insertions(+), 89 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 79f15509..e35c46be 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -32,55 +32,34 @@ def get_mode_int(self): def get_pid_int(self): return unhex(self.pid) - def compute(self, _data): - # _data will be the string returned from the car (ELM adapter). - # It should look something like this: - # - # Mode __Data___ - # | | | - # "\r\r48 6B 10 41 00 BE 1F B8 11 AA\r\r" - # || || || - # ECU PID Checksum + def compute(self, messages): - # create the response object with the raw data recieved - r = Response(_data) - - # split by lines, and remove empty lines - lines = filter(bool, re.split("[\r\n]", _data)) - - # splits each line by spaces (each element should be a hex byte) - lines = [line.split() for line in lines] + _bytes = [] - # filter by minimum response length (number of space delimited chunks (bytes)) - lines = filter(lambda line: len(line) >= 7, lines) + if len(messages) == 1: + _bytes = messages[0].data_bytes + else: + pass - if len(lines) > 1: - # filter for ECU 10 (engine) - lines = filter(lambda line: line[2] == '10', lines) - # by now, we should have only one line. - # Any more, and its a multiline response (which this library can't handle yet) - if len(lines) == 0: - debug("no valid data returned") - elif len(lines) > 1: - debug("multiline response returned, can't handle that (yet)") - else: # len(lines) == 1 + # create the response object with the raw data recieved + r = Response(message) - # combine the bytes back into a hex string, excluding the header + mode + pid, and trailing checksum - _data = "".join(lines[0][5:-1]) + # combine the bytes back into a hex string, excluding the header + mode + pid, and trailing checksum + _bytes = "".join(lines[0][5:-1]) - if ("NODATA" not in _data) and isHex(_data): + if ("NODATA" not in _data) and isHex(_data): - # constrain number of bytes in response - if (self.bytes > 0): # zero bytes means flexible response - _data = constrainHex(_data, self.bytes) + # constrain number of bytes in response + if (self.bytes > 0): # zero bytes means flexible response + _data = constrainHex(_data, self.bytes) - # decoded value into the response object - r.set(self.decode(_data)) + # decoded value into the response object + r.set(self.decode(_data)) - else: - # not a parseable response - debug("return data could not be decoded") + else: + # not a parseable response + debug("return data could not be decoded") return r diff --git a/obd/obd.py b/obd/obd.py index a0663811..4c77c9dc 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -29,7 +29,7 @@ ######################################################################## import time -from port import OBDPort, State +from port import OBDPort from commands import commands from utils import scanSerial, Response from debug import debug @@ -60,7 +60,7 @@ def connect(self, portstr=None): self.port = OBDPort(port) - if(self.port.state == State.Connected): + if self.port.is_connected(): # success! stop searching for serial break else: @@ -81,7 +81,6 @@ def close(self): self.port = None - # checks the port state for conncetion status def is_connected(self): return (self.port is not None) and self.port.is_connected() @@ -145,15 +144,17 @@ def supports(self, c): def send(self, c): """ send the given command, retrieve and parse response """ - # check for a connection if not self.is_connected(): debug("Query failed, no connection available", True) return Response() # return empty response - # send the query debug("Sending command: %s" % str(c)) - r = self.port.write_and_read(c.get_command()) # send command and retrieve response - return c.compute(r) # compute a response object + + c_str = c.get_command() + m = self.port.send_and_parse(c_str) # send command and retrieve message + r = c.compute(m) # compute a response object + + return r def query(self, c, force=False): diff --git a/obd/port.py b/obd/port.py index ca43491e..e9ec4c70 100644 --- a/obd/port.py +++ b/obd/port.py @@ -29,16 +29,12 @@ ######################################################################## import serial -import string import time -from utils import Response, unhex +import protocols +from utils import strip from debug import debug -class State(): - """ Enum for connection states """ - Unconnected, Connected = range(2) - class OBDPort: """ OBDPort abstracts all communication with OBD-II device.""" @@ -46,9 +42,9 @@ class OBDPort: def __init__(self, portname): """Initializes port by resetting device and gettings supported PIDs. """ - self.ELMver = "Unknown" - self.state = State.Unconnected - self.port = None + self.connected = False + self.port = None + self.protocol = None # ------------- open port ------------- @@ -71,40 +67,58 @@ def __init__(self, portname): debug("Serial port successfully opened on " + self.get_port_name()) - # ------------- atz (reset) ------------- + + # ---------------------------- ATZ (reset) ---------------------------- try: - r = self.write_and_read("atz", 1) # wait 1 second for ELM to initialize - r = self.__strip(r) - if not r: - self.__error("atz (reset) did not return with an ELM version") - return - self.ELMver = r - + r = self.send("ATZ", delay=1) # wait 1 second for ELM to initialize + # return data can be junk, so don't bother checking except serial.SerialException as e: self.__error(e) return - # ------------- ate0 (echo OFF) ------------- - r = self.write_and_read("ate0") - r = self.__strip(r) + + # -------------------------- ATE0 (echo OFF) -------------------------- + r = self.send("ATE0") + r = strip(r) if (len(r) < 2) or (r[-2:] != "OK"): - self.__error("ate0 did not return 'OK'") + self.__error("ATE0 did not return 'OK'") return - # ------------- ath1 (headers ON) ------------- - r = self.write_and_read("ath1") - r = self.__strip(r) + + # ------------------------- ATH1 (headers ON) ------------------------- + r = self.send("ATH1") + r = strip(r) if r != 'OK': - self.__error("ath1 did not return 'OK', or echoing is still ON") + self.__error("ATH1 did not return 'OK', or echoing is still ON") return - # ------------- done ------------- - debug("Connection successful") - self.state = State.Connected + + # ----------------------- ATSP0 (protocol AUTO) ----------------------- + r = self.send("ATSP0") + r = strip(r) + if r != 'OK': + self.__error("ATSP0 did not return 'OK'") + return - def __strip(self, s): - return "".join(s.split()) + # -------------- 0100 (first command, SEARCH protocols) -------------- + r = self.send("0100", delay=1) # give it a second to search + + + # ----------------------- ATDP (list protocol) ----------------------- + r = self.send("ATDP") + r = strip(r) + protocol_class = protocols.get(r) # lookup the protocol by name + if protocol_class is None: + self.__error("ELM responded with unknown protocol") + return + + self.protocol = protocol_class() + + + # ------------------------------- done ------------------------------- + debug("Connection successful") + self.connected = True def __error(self, msg=None): @@ -118,7 +132,7 @@ def __error(self, msg=None): if self.port is not None: self.port.close() - self.state = State.Unconnected + self.connected = False def get_port_name(self): @@ -126,28 +140,45 @@ def get_port_name(self): def is_connected(self): - return self.state == State.Connected + return self.connected def close(self): """ Resets device and closes all associated filehandles""" - if (self.port != None) and (self.state == State.Connected): - self.write("atz") + if (self.port != None) and self.connected: + self.__write("ATZ") self.port.close() - self.port = None - self.ELMver = "Unknown" + self.connected = False + self.port = None + self.protocol = None + + def send_and_parse(self, cmd, delay=None): + + r = self.send(cmd, delay) - def write_and_read(self, cmd, delay=None): + messages = self.protocol(r) # parses string into list of messages + + # if more than one ECUs have responded, pick the primary + # TODO: add support for more ECU types + if len(messages) > 1: + messages = filter(lambda m: m.tx_id == self.protocol.PRIMARY_ECU, messages) + + return messages[0] + + + def send(self, cmd, delay=None): self.__write(cmd) if delay is not None: time.sleep(delay) - return self.__read() + r = self.__read() + + return r # return raw string only # sends the hex string to the port @@ -162,7 +193,8 @@ def __write(self, cmd): debug("cannot perform write() when unconnected", True) - # accumulates and returns the ports response + # accumulates until the prompt character is seen + # returns raw string def __read(self): attempts = 2 diff --git a/obd/protocols/message.py b/obd/protocols/message.py index 5d92eb00..f377a6c3 100644 --- a/obd/protocols/message.py +++ b/obd/protocols/message.py @@ -1,7 +1,8 @@ class Message(object): - def __init__(self, frames, tx_id): + def __init__(self, raw, frames, tx_id): + self.raw = raw self.frames = frames self.tx_id = tx_id self.data_bytes = [] diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index c1da6254..e8083f54 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -14,8 +14,11 @@ class Protocol(object): + + PRIMARY_ECU = None + def __init__(self, baud=38400): - self.baud = baud + self.baud = baud def __call__(self, raw): @@ -44,7 +47,7 @@ def __call__(self, raw): messages = [] for ecu in ecus: - message = Message(ecus[ecu], ecu) + message = Message(raw, ecus[ecu], ecu) # subclass function to assemble frames into data_bytes self.parse_message(message) messages.append(message) diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 80e3fd0c..81e53aaf 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -7,6 +7,8 @@ class CANProtocol(Protocol): + PRIMARY_ECU = 0 + def __init__(self, baud, id_bits): Protocol.__init__(self, baud) self.id_bits = id_bits diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 62120757..7e39c39d 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -7,6 +7,8 @@ class LegacyProtocol(Protocol): + PRIMARY_ECU = 0x10 + def __init__(self, baud): Protocol.__init__(self, baud) diff --git a/obd/utils.py b/obd/utils.py index c225a5cf..45cce92f 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -96,6 +96,9 @@ def ascii_to_bytes(a): b.append(int(a[i:i+2], 16)) return b +def strip(): + return "".join(s.split()) + def unhex(_hex): _hex = "0" if _hex == "" else _hex return int(_hex, 16) From 1b49819e5de9a54e04830288c27dfd4dff96379a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 10 Feb 2015 18:32:52 -0500 Subject: [PATCH 165/569] accidently commited experimental output --- obd/protocols/proto_names.txt | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 obd/protocols/proto_names.txt diff --git a/obd/protocols/proto_names.txt b/obd/protocols/proto_names.txt deleted file mode 100644 index 16fb4a64..00000000 --- a/obd/protocols/proto_names.txt +++ /dev/null @@ -1,34 +0,0 @@ - -AT SP 1 -'SAE J1850 PWM\r\r' - -AT SP 2 -'SAE J1850 VPW\r\r' - -AT SP 3 -'ISO 9141-2\r\r' - -AT SP 4 -'ISO 14230-4 (KWP 5BAUD)\r\r' - -AT SP 5 -'ISO 14230-4 (KWP FAST)\r\r' - -AT SP 6 -'ISO 15765-4 (CAN 11/500)\r\r' - -AT SP 7 -'ISO 15765-4 (CAN 29/500)\r\r' - -AT SP 8 -'ISO 15765-4 (CAN 11/250)\r\r' - -AT SP 9 -'ISO 15765-4 (CAN 29/250)\r\r' - -AT SP A -'SAE J1939 (CAN 29/250)\r\r' - -AT SP B -'USER1 (CAN 11/125)\r\r' - From 5e328ed2dc10919d840a1ea14c86e6353c9f9340 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 13 Feb 2015 21:17:59 -0500 Subject: [PATCH 166/569] continuing to refine parent/subclass relationship --- obd/debug.py | 5 ++ obd/protocols/frame.py | 10 ---- obd/protocols/message.py | 8 --- obd/protocols/protocol.py | 86 ++++++++++++++++++++++++-------- obd/protocols/protocol_can.py | 38 ++++++++++---- obd/protocols/protocol_legacy.py | 23 +++++---- 6 files changed, 114 insertions(+), 56 deletions(-) delete mode 100644 obd/protocols/frame.py delete mode 100644 obd/protocols/message.py diff --git a/obd/debug.py b/obd/debug.py index 0dc5ff4f..be8dae50 100644 --- a/obd/debug.py +++ b/obd/debug.py @@ -42,3 +42,8 @@ def __call__(self, msg, forcePrint=False): self.handler(msg) debug = Debug() + + +class ProtocolError(Exception): + def __init__(self): + pass diff --git a/obd/protocols/frame.py b/obd/protocols/frame.py deleted file mode 100644 index 0905e500..00000000 --- a/obd/protocols/frame.py +++ /dev/null @@ -1,10 +0,0 @@ - - -class Frame(object): - def __init__(self, raw): - self.raw = raw - self.data_bytes = [] - self.priority = None - self.addr_mode = None - self.rx_id = None - self.tx_id = None diff --git a/obd/protocols/message.py b/obd/protocols/message.py deleted file mode 100644 index f377a6c3..00000000 --- a/obd/protocols/message.py +++ /dev/null @@ -1,8 +0,0 @@ - - -class Message(object): - def __init__(self, raw, frames, tx_id): - self.raw = raw - self.frames = frames - self.tx_id = tx_id - self.data_bytes = [] diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index e8083f54..885208c0 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -1,17 +1,45 @@ +import re +from obd.utils import ascii_to_bytes +from obd.debug import debug + + """ -Protocol objects are stateless factories for Frames and Messages. -They are __called__ with the raw string response, and -return a list of Messages. +Basic data models for all protocols to use """ -from frame import Frame -from message import Message -import re + +class Frame(object): + def __init__(self, raw): + self.raw = raw + self.data_bytes = [] + self.priority = None + self.addr_mode = None + self.rx_id = None + self.tx_id = None + self.type = None + self.seq_id = 0 + self.msg_len = None +class Message(object): + def __init__(self, frames, tx_id): + self.frames = frames + self.tx_id = tx_id + self.data_bytes = [] + + + + +""" + +Protocol objects are stateless factories for Frames and Messages. +They are __called__ with the raw string response, and return a +list of Messages. + +""" class Protocol(object): @@ -29,12 +57,14 @@ def __call__(self, raw): # ditch spaces lines = [line.replace(' ', '') for line in lines] - # create frame objects for each line - frames = [Frame(line) for line in lines] + frames = [] + for line in lines: + # subclass function to parse the lines into Frames + frame = self.create_frame(line) - # subclass function to load the frame parameters - for frame in frames: - self.parse_frame(frame) + # drop frames that couldn't be parsed + if frame is not None: + frames.append(frame) # group frames by transmitting ECU (tx_id) ecus = {} @@ -45,20 +75,36 @@ def __call__(self, raw): ecus[frame.tx_id].append(frame) messages = [] - for ecu in ecus: - message = Message(raw, ecus[ecu], ecu) - # subclass function to assemble frames into data_bytes - self.parse_message(message) - messages.append(message) + # subclass function to assemble frames into Messages + message = self.create_message(ecus[ecu], ecu) + + # drop messages that couldn't be assembled + if message is not None: + messages.append(message) return messages - def parse_frame(self, frame): - """ override in subclass for each protocol """ + def create_frame(self, raw): + """ + override in subclass for each protocol + + Function recieves the raw string data for a frame. + + Function should return a Frame instance. If fatal errors were + found, this function should return None (the Frame is dropped). + """ raise NotImplementedError() - def parse_message(self, message): - """ override in subclass for each protocol """ + + def create_message(self, frames): + """ + override in subclass for each protocol + + Function recieves a list of Frame objects. + + Function should return a Message instance. If fatal errors were + found, this function should return None (the Message is dropped). + """ raise NotImplementedError() diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 81e53aaf..68e8aa62 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -1,26 +1,29 @@ - -from protocol import Protocol -from obd.utils import ascii_to_bytes -from obd.debug import debug +from protocol import * class CANProtocol(Protocol): PRIMARY_ECU = 0 + FRAME_TYPE_SF = 0x00 # single frame + FRAME_TYPE_FF = 0x10 # first frame of multi-frame message + FRAME_TYPE_CF = 0x20 # consecutive frame(s) of multi-frame message + + def __init__(self, baud, id_bits): Protocol.__init__(self, baud) self.id_bits = id_bits - def parse_frame(self, frame): + def create_frame(self, raw): # pad 11-bit CAN headers out to 32 bits for consistency, # since ELM already does this for 29-bit CAN headers if self.id_bits == 11: - frame.raw = "00000" + frame.raw + raw = "00000" + raw - raw_bytes = ascii_to_bytes(frame.raw) + frame = Frame(raw) + raw_bytes = ascii_to_bytes(raw) # read header information if self.id_bits == 11: @@ -45,14 +48,31 @@ def parse_frame(self, frame): frame.rx_id = raw_bytes[2] # 0x33 = broadcast (functional) frame.tx_id = raw_bytes[3] # 0xF1 = tester ID - frame.data_bytes = raw_bytes[5:] + frame.data_bytes = raw_bytes[4:] + + + # extra frame info in data section + frame.type = frame.data_bytes[0] & 0xF0 + if frame.type is not in [self.FRAME_TYPE_CF, + self.FRAME_TYPE_FF, + self.FRAME_TYPE_SF]: + return None + + return frame + + + def create_message(self, frames, tx_id): + + message = Message(frames, tx_id) - def parse_message(self, message): if len(message.frames) == 1: message.data_bytes = message.frames[0].data_bytes else: debug("Recieved multi-frame response. Can't parse those yet") + return None + + return message ############################################## diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 7e39c39d..70579813 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -1,8 +1,5 @@ - -from protocol import Protocol -from obd.utils import ascii_to_bytes -from obd.debug import debug +from protocol import * class LegacyProtocol(Protocol): @@ -12,8 +9,10 @@ class LegacyProtocol(Protocol): def __init__(self, baud): Protocol.__init__(self, baud) - def parse_frame(self, frame): - raw_bytes = ascii_to_bytes(frame.raw) + def create_frame(self, raw): + + frame = Frame(raw) + raw_bytes = ascii_to_bytes(raw) frame.data_bytes = raw_bytes[3:-1] # exclude trailing checksum (handled by ELM adapter) @@ -22,13 +21,19 @@ def parse_frame(self, frame): frame.rx_id = raw_bytes[1] frame.tx_id = raw_bytes[2] - def parse_message(self, message): - if len(message.frames) == 1: + return frame + + def create_message(self, frames, tx_id): + + message = Message(frames, tx_id) + + if len(frames) == 1: message.data_bytes = message.frames[0].data_bytes else: debug("Recieved multi-frame response. Can't parse those yet") + return None - + return message From 389374dff05f580ebd722b8e85d9e7ed583d3363 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 13 Feb 2015 22:42:10 -0500 Subject: [PATCH 167/569] using AT DPN for protocol detection --- obd/{port.py => elm327.py} | 42 +++++++++++++++++++++++++---------- obd/obd.py | 8 +++---- obd/protocols/__init__.py | 22 ------------------ obd/protocols/protocol_can.py | 4 ++-- 4 files changed, 36 insertions(+), 40 deletions(-) rename obd/{port.py => elm327.py} (91%) diff --git a/obd/port.py b/obd/elm327.py similarity index 91% rename from obd/port.py rename to obd/elm327.py index e9ec4c70..3fdb466f 100644 --- a/obd/port.py +++ b/obd/elm327.py @@ -30,14 +30,30 @@ import serial import time -import protocols +from protocols import * from utils import strip from debug import debug -class OBDPort: - """ OBDPort abstracts all communication with OBD-II device.""" +class ELM327: + """ ELM327 abstracts all communication with OBD-II device.""" + + _SUPPORTED_PROTOCOLS = { + "0" : None, + "1" : SAE_J1850_PWM, + "2" : SAE_J1850_VPW, + "3" : ISO_9141_2, + "4" : ISO_14230_4_5baud, + "5" : ISO_14230_4_fast, + "6" : ISO_15765_4_11bit_500k, + "7" : ISO_15765_4_29bit_500k, + "8" : ISO_15765_4_11bit_250k, + "9" : ISO_15765_4_29bit_250k, + "A" : SAE_J1939, + "B" : None, + "C" : None, + } def __init__(self, portname): """Initializes port by resetting device and gettings supported PIDs. """ @@ -80,7 +96,7 @@ def __init__(self, portname): # -------------------------- ATE0 (echo OFF) -------------------------- r = self.send("ATE0") r = strip(r) - if (len(r) < 2) or (r[-2:] != "OK"): + if not r.endswith("OK"): self.__error("ATE0 did not return 'OK'") return @@ -105,15 +121,17 @@ def __init__(self, portname): r = self.send("0100", delay=1) # give it a second to search - # ----------------------- ATDP (list protocol) ----------------------- - r = self.send("ATDP") + # ------------------- ATDPN (list protocol number) ------------------- + r = self.send("ATDPN") r = strip(r) - protocol_class = protocols.get(r) # lookup the protocol by name - if protocol_class is None: + # suppress any "automatic" prefix + r = r[1:] if (len(r) > 1 and r.startswith("A")) else r + + if r not in _SUPPORTED_PROTOCOLS: self.__error("ELM responded with unknown protocol") return - self.protocol = protocol_class() + self.protocol = _SUPPORTED_PROTOCOLS[r]() # ------------------------------- done ------------------------------- @@ -128,10 +146,10 @@ def __error(self, msg=None): if msg is not None: debug(' ' + str(msg), True) - + if self.port is not None: self.port.close() - + self.connected = False @@ -156,7 +174,7 @@ def close(self): def send_and_parse(self, cmd, delay=None): - + r = self.send(cmd, delay) messages = self.protocol(r) # parses string into list of messages diff --git a/obd/obd.py b/obd/obd.py index 4c77c9dc..ffa32042 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -29,7 +29,7 @@ ######################################################################## import time -from port import OBDPort +from elm327 import ELM327 from commands import commands from utils import scanSerial, Response from debug import debug @@ -49,7 +49,7 @@ def __init__(self, portstr=None): def connect(self, portstr=None): - """ attempts to instantiate an OBDPort object. Loads commands on success""" + """ attempts to instantiate an ELM327 object. Loads commands on success""" if portstr is None: debug("Using scanSerial to select port") @@ -58,14 +58,14 @@ def connect(self, portstr=None): for port in portnames: - self.port = OBDPort(port) + self.port = ELM327(port) if self.port.is_connected(): # success! stop searching for serial break else: debug("Explicit port defined") - self.port = OBDPort(portstr) + self.port = ELM327(portstr) # if a connection was made, query for commands if self.is_connected(): diff --git a/obd/protocols/__init__.py b/obd/protocols/__init__.py index a2246f60..6d5aa255 100644 --- a/obd/protocols/__init__.py +++ b/obd/protocols/__init__.py @@ -10,25 +10,3 @@ ISO_15765_4_11bit_250k, \ ISO_15765_4_29bit_250k, \ SAE_J1939 - - -# allow each class to be access by ELM name (the result of an "AT DP\r") -protocols = { - "SAE J1850 PWM" : SAE_J1850_PWM, - "SAE J1850 VPW" : SAE_J1850_VPW, - "ISO 9141-2" : ISO_9141_2, - "ISO 14230-4 (KWP 5BAUD)" : ISO_14230_4_5baud, - "ISO 14230-4 (KWP FAST)" : ISO_14230_4_fast, - "ISO 15765-4 (CAN 11/500)" : ISO_15765_4_11bit_500k, - "ISO 15765-4 (CAN 29/500)" : ISO_15765_4_29bit_500k, - "ISO 15765-4 (CAN 11/250)" : ISO_15765_4_11bit_250k, - "ISO 15765-4 (CAN 29/250)" : ISO_15765_4_29bit_250k, - "SAE J1939" : SAE_J1939 -} - - -def get(name): - if name in protocols: - return protocols[name] - else: - return None diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 68e8aa62..bb75b98a 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -54,7 +54,7 @@ def create_frame(self, raw): # extra frame info in data section frame.type = frame.data_bytes[0] & 0xF0 - if frame.type is not in [self.FRAME_TYPE_CF, + if frame.type not in [self.FRAME_TYPE_CF, self.FRAME_TYPE_FF, self.FRAME_TYPE_SF]: return None @@ -67,7 +67,7 @@ def create_message(self, frames, tx_id): message = Message(frames, tx_id) if len(message.frames) == 1: - message.data_bytes = message.frames[0].data_bytes + message.data_bytes = message.frames[0].data_bytes[1:] else: debug("Recieved multi-frame response. Can't parse those yet") return None From e9af33bec27b386bfa857cf09809ffd88855a073 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 13 Feb 2015 23:02:20 -0500 Subject: [PATCH 168/569] updated GPL notice, added Peter Creath --- obd/OBDCommand.py | 31 ++++++++++++++++++++++++++++++- obd/__init__.py | 3 ++- obd/async.py | 3 ++- obd/codes.py | 3 ++- obd/commands.py | 3 ++- obd/debug.py | 3 ++- obd/decoders.py | 3 ++- obd/elm327.py | 3 ++- obd/obd.py | 3 ++- obd/protocols/__init__.py | 30 ++++++++++++++++++++++++++++++ obd/protocols/protocol.py | 30 ++++++++++++++++++++++++++++++ obd/protocols/protocol_can.py | 32 +++++++++++++++++++++++++++++++- obd/protocols/protocol_legacy.py | 30 ++++++++++++++++++++++++++++++ obd/utils.py | 3 ++- 14 files changed, 169 insertions(+), 11 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index e35c46be..3e78ffee 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -1,10 +1,39 @@ +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2009 Peter J. Creath # +# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# # +######################################################################## +# # +# OBDCommand.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## + import re from utils import * from debug import debug - class OBDCommand(): def __init__(self, name, desc, mode, pid, returnBytes, decoder, supported=False): self.name = name diff --git a/obd/__init__.py b/obd/__init__.py index 518a5f21..13c71544 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -5,7 +5,8 @@ # # # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # -# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2009 Peter J. Creath # +# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # # # ######################################################################## # # diff --git a/obd/async.py b/obd/async.py index 8854c033..7459271f 100644 --- a/obd/async.py +++ b/obd/async.py @@ -5,7 +5,8 @@ # # # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # -# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2009 Peter J. Creath # +# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # # # ######################################################################## # # diff --git a/obd/codes.py b/obd/codes.py index 821d1493..44bb9e34 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -5,7 +5,8 @@ # # # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # -# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2009 Peter J. Creath # +# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # # # ######################################################################## # # diff --git a/obd/commands.py b/obd/commands.py index 1cdfb229..7b223594 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -5,7 +5,8 @@ # # # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # -# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2009 Peter J. Creath # +# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # # # ######################################################################## # # diff --git a/obd/debug.py b/obd/debug.py index be8dae50..0bd9c696 100644 --- a/obd/debug.py +++ b/obd/debug.py @@ -5,7 +5,8 @@ # # # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # -# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2009 Peter J. Creath # +# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # # # ######################################################################## # # diff --git a/obd/decoders.py b/obd/decoders.py index 8c97befd..7e73a3c0 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -5,7 +5,8 @@ # # # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # -# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2009 Peter J. Creath # +# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # # # ######################################################################## # # diff --git a/obd/elm327.py b/obd/elm327.py index 3fdb466f..e2e3d922 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -5,7 +5,8 @@ # # # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # -# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2009 Peter J. Creath # +# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # # # ######################################################################## # # diff --git a/obd/obd.py b/obd/obd.py index ffa32042..ae41e53d 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -5,7 +5,8 @@ # # # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # -# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2009 Peter J. Creath # +# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # # # ######################################################################## # # diff --git a/obd/protocols/__init__.py b/obd/protocols/__init__.py index 6d5aa255..0a07de65 100644 --- a/obd/protocols/__init__.py +++ b/obd/protocols/__init__.py @@ -1,4 +1,34 @@ +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2009 Peter J. Creath # +# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# # +######################################################################## +# # +# protocols/__init__.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## + from protocol_legacy import SAE_J1850_PWM, \ SAE_J1850_VPW, \ ISO_9141_2, \ diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 885208c0..903e611f 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -1,4 +1,34 @@ +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2009 Peter J. Creath # +# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# # +######################################################################## +# # +# protocols/protocol.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## + import re from obd.utils import ascii_to_bytes from obd.debug import debug diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index bb75b98a..1a27aded 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -1,4 +1,34 @@ +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2009 Peter J. Creath # +# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# # +######################################################################## +# # +# protocols/protocol_can.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## + from protocol import * @@ -67,7 +97,7 @@ def create_message(self, frames, tx_id): message = Message(frames, tx_id) if len(message.frames) == 1: - message.data_bytes = message.frames[0].data_bytes[1:] + message.data_bytes = message.frames[0].data_bytes[1:] # ignore PCI byte else: debug("Recieved multi-frame response. Can't parse those yet") return None diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 70579813..15d6cc95 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -1,4 +1,34 @@ +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2009 Peter J. Creath # +# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# # +######################################################################## +# # +# protocols/protocol_legacy.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## + from protocol import * diff --git a/obd/utils.py b/obd/utils.py index 45cce92f..63abc30e 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -5,7 +5,8 @@ # # # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # -# Copyright 2014 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2009 Peter J. Creath # +# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # # # ######################################################################## # # From e12efdb7903aa6966a5705b1f0dd3437508f4f42 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 14 Feb 2015 22:44:23 -0500 Subject: [PATCH 169/569] started writing tests for protocol system --- obd/OBDCommand.py | 35 ++++------ obd/elm327.py | 49 +++++++------- obd/protocols/protocol.py | 14 +++- obd/protocols/protocol_can.py | 3 + obd/protocols/protocol_legacy.py | 3 + obd/utils.py | 4 +- tests/test_OBD.py | 13 ++-- tests/test_OBDCommand.py | 5 ++ tests/test_protocols.py | 108 +++++++++++++++++++++++++++++++ 9 files changed, 180 insertions(+), 54 deletions(-) create mode 100644 tests/test_protocols.py diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 3e78ffee..a2865149 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -61,34 +61,25 @@ def get_mode_int(self): def get_pid_int(self): return unhex(self.pid) - def compute(self, messages): - - _bytes = [] - - if len(messages) == 1: - _bytes = messages[0].data_bytes - else: - pass - + def compute(self, message): # create the response object with the raw data recieved r = Response(message) - # combine the bytes back into a hex string, excluding the header + mode + pid, and trailing checksum - _bytes = "".join(lines[0][5:-1]) + # combine the bytes back into a hex string + # TODO: rewrite decoders to handle raw byte arrays + _data = "" + for b in message.data_bytes: + h = hex(b)[2:].upper() + h = "0" + h if len(h) < 2 else h + _data += h - if ("NODATA" not in _data) and isHex(_data): + # constrain number of bytes in response + if (self.bytes > 0): # zero bytes means flexible response + _data = constrainHex(_data, self.bytes) - # constrain number of bytes in response - if (self.bytes > 0): # zero bytes means flexible response - _data = constrainHex(_data, self.bytes) - - # decoded value into the response object - r.set(self.decode(_data)) - - else: - # not a parseable response - debug("return data could not be decoded") + # decoded value into the response object + r.set(self.decode(_data)) return r diff --git a/obd/elm327.py b/obd/elm327.py index e2e3d922..a6db6dea 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -59,16 +59,16 @@ class ELM327: def __init__(self, portname): """Initializes port by resetting device and gettings supported PIDs. """ - self.connected = False - self.port = None - self.protocol = None + self.__connected = False + self.__port = None + self.__protocol = None # ------------- open port ------------- debug("Opening serial port '%s'" % portname) try: - self.port = serial.Serial(portname, \ + self.__port = serial.Serial(portname, \ baudrate = 38400, \ parity = serial.PARITY_NONE, \ stopbits = 1, \ @@ -132,12 +132,12 @@ def __init__(self, portname): self.__error("ELM responded with unknown protocol") return - self.protocol = _SUPPORTED_PROTOCOLS[r]() + self.__protocol = _SUPPORTED_PROTOCOLS[r]() # ------------------------------- done ------------------------------- debug("Connection successful") - self.connected = True + self.__connected = True def __error(self, msg=None): @@ -148,42 +148,42 @@ def __error(self, msg=None): if msg is not None: debug(' ' + str(msg), True) - if self.port is not None: - self.port.close() + if self.__port is not None: + self.__port.close() - self.connected = False + self.__connected = False def get_port_name(self): - return self.port.portstr if (self.port is not None) else "No Port" + return self.__port.portstr if (self.__port is not None) else "No Port" def is_connected(self): - return self.connected + return self.__connected def close(self): """ Resets device and closes all associated filehandles""" - if (self.port != None) and self.connected: + if (self.__port != None) and self.__connected: self.__write("ATZ") - self.port.close() + self.__port.close() - self.connected = False - self.port = None - self.protocol = None + self.__connected = False + self.__port = None + self.__protocol = None def send_and_parse(self, cmd, delay=None): r = self.send(cmd, delay) - messages = self.protocol(r) # parses string into list of messages + messages = self.__protocol(r) # parses string into list of messages # if more than one ECUs have responded, pick the primary # TODO: add support for more ECU types if len(messages) > 1: - messages = filter(lambda m: m.tx_id == self.protocol.PRIMARY_ECU, messages) + messages = filter(lambda m: m.tx_id == self.__protocol.PRIMARY_ECU, messages) return messages[0] @@ -202,11 +202,12 @@ def send(self, cmd, delay=None): # sends the hex string to the port def __write(self, cmd): - if self.port: + + if self.__port: cmd += "\r\n" # terminate - self.port.flushOutput() - self.port.flushInput() - self.port.write(cmd) + self.__port.flushOutput() + self.__port.flushInput() + self.__port.write(cmd) debug("write: " + repr(cmd)) else: debug("cannot perform write() when unconnected", True) @@ -219,9 +220,9 @@ def __read(self): attempts = 2 result = "" - if self.port: + if self.__port: while True: - c = self.port.read(1) + c = self.__port.read(1) # if nothing was recieved if not c: diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 903e611f..ee82b2ef 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -30,7 +30,7 @@ ######################################################################## import re -from obd.utils import ascii_to_bytes +from obd.utils import ascii_to_bytes, isHex from obd.debug import debug @@ -60,6 +60,15 @@ def __init__(self, frames, tx_id): self.tx_id = tx_id self.data_bytes = [] + def __eq__(self, other): + if isinstance(other, Message): + for attr in ["frames", "tx_id", "data_bytes"]: + if getattr(self, attr) != getattr(other, attr): + return False + return True + else: + return False + @@ -87,6 +96,9 @@ def __call__(self, raw): # ditch spaces lines = [line.replace(' ', '') for line in lines] + # ditch frames without valid hex (trashes "NO DATA", etc...) + lines = filter(isHex, lines) + frames = [] for line in lines: # subclass function to parse the lines into Frames diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 1a27aded..e4a4fdae 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -55,6 +55,9 @@ def create_frame(self, raw): frame = Frame(raw) raw_bytes = ascii_to_bytes(raw) + print raw + print raw_bytes + # read header information if self.id_bits == 11: frame.priority = raw_bytes[2] & 0x0F # always 7 diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 15d6cc95..590479b2 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -44,6 +44,9 @@ def create_frame(self, raw): frame = Frame(raw) raw_bytes = ascii_to_bytes(raw) + if len(raw_bytes) < 5: + return None + frame.data_bytes = raw_bytes[3:-1] # exclude trailing checksum (handled by ELM adapter) # read header information diff --git a/obd/utils.py b/obd/utils.py index 63abc30e..9387486f 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -62,14 +62,14 @@ class Unit: class Response(): - def __init__(self, raw_data=""): + def __init__(self, raw_data=None): self.value = None self.unit = Unit.NONE self.raw_data = raw_data self.time = time.time() def is_null(self): - return (len(self.raw_data) == 0) or (self.value == None) + return (self.raw_data == None) or (self.value == None) def set(self, decode): self.value = decode[0] diff --git a/tests/test_OBD.py b/tests/test_OBD.py index aa0a1ddb..d95de759 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -3,6 +3,7 @@ from obd.utils import Response from obd.commands import OBDCommand from obd.decoders import noop +from obd.protocols import SAE_J1850_PWM def test_is_connected(): @@ -12,11 +13,12 @@ def test_is_connected(): # todo +# TODO: rewrite for new protocol architecture def test_query(): # we don't need an actual serial connection o = obd.OBD("/dev/null") # forge our own command, to control the output - cmd = OBDCommand("", "", "01", "23", 2, noop) + cmd = OBDCommand("TEST", "Test command", "01", "23", 2, noop) # forge IO from the car by overwriting the read/write functions @@ -28,9 +30,11 @@ def write(cmd): toCar[0] = cmd o.is_connected = lambda *args: True - o.port.port = True - o.port._OBDPort__write = write - o.port._OBDPort__read = lambda *args: fromCar + o.port.is_connected = lambda *args: True + + o.port._ELM327__protocol = SAE_J1850_PWM() + o.port._ELM327__write = write + o.port._ELM327__read = lambda *args: fromCar # make sure unsupported commands don't write ------------------------------ fromCar = "48 6B 10 41 23 AB CD 10\r\r" @@ -105,6 +109,5 @@ def write(cmd): assert r.raw_data == fromCar assert r.is_null() - def test_load_commands(): pass diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index c79ce254..229b8c85 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -36,6 +36,9 @@ def test_clone(): assert cmd.supported == cmd.supported +# TODO: rewrite these for new commands accepting messages (rather than strings) +""" + def test_data_stripping(): # name description mode cmd bytes decoder cmd = OBDCommand("Test", "example OBD command", "01", "00", 2, noop) @@ -58,3 +61,5 @@ def test_data_length(): assert r.value == "0123" r = cmd.compute("48 6B 10 41 00 01 10\r\n") assert r.value == "0100" + +""" \ No newline at end of file diff --git a/tests/test_protocols.py b/tests/test_protocols.py new file mode 100644 index 00000000..4396ffc9 --- /dev/null +++ b/tests/test_protocols.py @@ -0,0 +1,108 @@ + +from obd.protocols import * +from obd.protocols.protocol import Message + + +LEGACY_PROTOCOLS = [ + SAE_J1850_PWM, + SAE_J1850_VPW, + ISO_9141_2, + ISO_14230_4_5baud, + ISO_14230_4_fast +] + +CAN_11_PROTOCOLS = [ + ISO_15765_4_11bit_500k, + ISO_15765_4_11bit_250k, +] + +CAN_29_PROTOCOLS = [ + ISO_15765_4_29bit_500k, + ISO_15765_4_29bit_250k, + SAE_J1939 +] + + +def check_message(m, num_frames, tx_id, data_bytes): + """ generic test for correct message values """ + assert len(m.frames) == num_frames + assert m.tx_id == tx_id + assert m.data_bytes == data_bytes + + +def test_legacy(): + for protocol in LEGACY_PROTOCOLS: + p = protocol() + + # single frame cases + + r = p("48 6B 10 41 00 BE 1F B8 11 AA\r\r") + assert len(r) == 1 + check_message(r[0], 1, 16, [65, 0, 190, 31, 184, 17]) + + r = p("48 6B 10 41 00 BE 1F B8 11 AA") + assert len(r) == 1 + check_message(r[0], 1, 16, [65, 0, 190, 31, 184, 17]) + + r = p("NO DATA") + assert len(r) == 0 + + r = p("TOTALLY NOT HEX") + assert len(r) == 0 + + # multi-frame cases + + # seperate ECUs, single frames each + r = p("48 6B 10 41 00 BE 1F B8 11 AA\r\r48 6B 11 41 00 01 02 03 04 AA\r\r") + assert len(r) == 2 + check_message(r[0], 1, 16, [65, 0, 190, 31, 184, 17]) + check_message(r[1], 1, 17, [65, 0, 1, 2, 3, 4 ]) + + r = p("NO DATA\r\r48 6B 10 41 00 BE 1F B8 11 AA\r\r") + assert len(r) == 1 + check_message(r[0], 1, 16, [65, 0, 190, 31, 184, 17]) + + r = p("NO DATA\r\rNO DATA\r\r") + assert len(r) == 0 + + +def test_can_11(): + for protocol in CAN_11_PROTOCOLS: + p = protocol() + + # single frame cases + + r = p("7E8 06 41 00 BE 7F B8 13\r\r") + assert len(r) == 1 + check_message(r[0], 1, 0, [65, 0, 190, 127, 184, 19]) + + r = p("7E8 06 41 00 BE 7F B8 13") + assert len(r) == 1 + check_message(r[0], 1, 0, [65, 0, 190, 127, 184, 19]) + + r = p("NO DATA") + assert len(r) == 0 + + r = p("TOTALLY NOT HEX") + assert len(r) == 0 + + # multi-frame cases + + # seperate ECUs, single frames each + r = p("7E8 06 41 00 BE 7F B8 13 \r7EB 06 41 00 80 40 00 01 \r7EA 06 41 00 80 00 00 01 \r\r") + assert len(r) == 3 + # messages are returned in ECU order + check_message(r[0], 1, 0, [65, 0, 190, 127, 184, 19]) + check_message(r[1], 1, 2, [65, 0, 128, 0, 0, 1 ]) + check_message(r[2], 1, 3, [65, 0, 128, 64, 0, 1 ]) + + r = p("NO DATA\r\r7E8 06 41 00 BE 7F B8 13\r\r") + assert len(r) == 1 + check_message(r[0], 1, 0, [65, 0, 190, 127, 184, 19]) + + r = p("NO DATA\r\rNO DATA\r\r") + assert len(r) == 0 + + +def test_can_29(): + pass From fe681f59235c4af8e3ee42279e41f2df3bc5146c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 14 Feb 2015 22:46:30 -0500 Subject: [PATCH 170/569] removed silly print statement. Always check your diffs... --- obd/protocols/protocol_can.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index e4a4fdae..1a27aded 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -55,9 +55,6 @@ def create_frame(self, raw): frame = Frame(raw) raw_bytes = ascii_to_bytes(raw) - print raw - print raw_bytes - # read header information if self.id_bits == 11: frame.priority = raw_bytes[2] & 0x0F # always 7 From 85190598c39e08c5dc239d5d392437dac743538e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 15 Feb 2015 01:02:06 -0500 Subject: [PATCH 171/569] wrote detector for primary ECU and added better docstrings --- obd/OBDCommand.py | 2 +- obd/elm327.py | 203 ++++++++++++++++++++++++++-------------------- obd/obd.py | 25 ++++-- obd/utils.py | 9 ++ tests/test_OBD.py | 32 ++------ 5 files changed, 149 insertions(+), 122 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index a2865149..8263c526 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -69,7 +69,7 @@ def compute(self, message): # combine the bytes back into a hex string # TODO: rewrite decoders to handle raw byte arrays _data = "" - for b in message.data_bytes: + for b in message.data_bytes[2:]: h = hex(b)[2:].upper() h = "0" + h if len(h) < 2 else h _data += h diff --git a/obd/elm327.py b/obd/elm327.py index a6db6dea..c5abd0f4 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -32,16 +32,25 @@ import serial import time from protocols import * -from utils import strip +from utils import strip, numBitsSet from debug import debug class ELM327: - """ ELM327 abstracts all communication with OBD-II device.""" + """ + Provides interface for the vehicles primary ECU. + After instantiation with a portname (/dev/ttyUSB0, etc...), + the following functions become available: + + send_and_parse() + get_port_name() + is_connected() + close() + """ _SUPPORTED_PROTOCOLS = { - "0" : None, + #"0" : None, # automatic mode "1" : SAE_J1850_PWM, "2" : SAE_J1850_VPW, "3" : ISO_9141_2, @@ -52,16 +61,17 @@ class ELM327: "8" : ISO_15765_4_11bit_250k, "9" : ISO_15765_4_29bit_250k, "A" : SAE_J1939, - "B" : None, - "C" : None, + #"B" : None, # user defined 1 + #"C" : None, # user defined 2 } def __init__(self, portname): """Initializes port by resetting device and gettings supported PIDs. """ - self.__connected = False - self.__port = None - self.__protocol = None + self.__connected = False + self.__port = None + self.__protocol = None + self.__primary_ecu = None # message.tx_id # ------------- open port ------------- @@ -87,7 +97,7 @@ def __init__(self, portname): # ---------------------------- ATZ (reset) ---------------------------- try: - r = self.send("ATZ", delay=1) # wait 1 second for ELM to initialize + r = self.__send("ATZ", delay=1) # wait 1 second for ELM to initialize # return data can be junk, so don't bother checking except serial.SerialException as e: self.__error(e) @@ -95,7 +105,7 @@ def __init__(self, portname): # -------------------------- ATE0 (echo OFF) -------------------------- - r = self.send("ATE0") + r = self.__send("ATE0") r = strip(r) if not r.endswith("OK"): self.__error("ATE0 did not return 'OK'") @@ -103,7 +113,7 @@ def __init__(self, portname): # ------------------------- ATH1 (headers ON) ------------------------- - r = self.send("ATH1") + r = self.__send("ATH1") r = strip(r) if r != 'OK': self.__error("ATH1 did not return 'OK', or echoing is still ON") @@ -111,7 +121,7 @@ def __init__(self, portname): # ----------------------- ATSP0 (protocol AUTO) ----------------------- - r = self.send("ATSP0") + r = self.__send("ATSP0") r = strip(r) if r != 'OK': self.__error("ATSP0 did not return 'OK'") @@ -119,11 +129,11 @@ def __init__(self, portname): # -------------- 0100 (first command, SEARCH protocols) -------------- - r = self.send("0100", delay=1) # give it a second to search + r0100 = self.__send("0100", delay=1) # give it a second to search # ------------------- ATDPN (list protocol number) ------------------- - r = self.send("ATDPN") + r = self.__send("ATDPN") r = strip(r) # suppress any "automatic" prefix r = r[1:] if (len(r) > 1 and r.startswith("A")) else r @@ -135,11 +145,52 @@ def __init__(self, portname): self.__protocol = _SUPPORTED_PROTOCOLS[r]() + # Now that a protocol has been selected, we can figure out + # which ECU is the primary. + + m = self.__protocol(r0100) + self.__primary_ecu = self.__find_primary_ecu(m) + if self.__primary_ecu is None: + self.__error("Failed to choose primary ECU") + return + # ------------------------------- done ------------------------------- debug("Connection successful") self.__connected = True + def __find_primary_ecu(messages): + """ + Given a list of messages from different ECUS, + (in response to the 0100 PID listing command) + choose the ID of the primary ECU + """ + + if len(messages) == 0: + return None + elif len(messages) == 1: + return messages[0].tx_id + else: + # first, try filtering for the standard ECU IDs + test = lambda m: m.tx_id == self.__protocol.PRIMARY_ECU + + if bool(filter(test, messages)): + return self.__protocol.PRIMARY_ECU + else: + # last resort solution, choose ECU + # with the most PIDs supported + best = 0 + tx_id = None + + for message in messages: + bits = sum([numBitsSet(b) for b in message.data_bytes]) + if bits > best: + best = bits + tx_id = message.tx_id + + return tx_id + + def __error(self, msg=None): """ handles fatal failures, print debug info and closes serial """ @@ -159,36 +210,59 @@ def get_port_name(self): def is_connected(self): - return self.__connected + return self.__connected and (self.__port is not None) def close(self): - """ Resets device and closes all associated filehandles""" + """ + Resets the device, and clears all attributes to unconnected state + """ if (self.__port != None) and self.__connected: self.__write("ATZ") self.__port.close() - self.__connected = False - self.__port = None - self.__protocol = None + self.__connected = False + self.__port = None + self.__protocol = None + self.__primary_ecu = None def send_and_parse(self, cmd, delay=None): + """ + send() function used to service all OBDCommands - r = self.send(cmd, delay) + Sends the given command string (rejects "AT" command), + parses the response string with the appropriate protocol object. - messages = self.__protocol(r) # parses string into list of messages + Returns the Message object from the primary ECU, or None, + if no appropriate response was recieved. + """ - # if more than one ECUs have responded, pick the primary - # TODO: add support for more ECU types - if len(messages) > 1: - messages = filter(lambda m: m.tx_id == self.__protocol.PRIMARY_ECU, messages) + if "AT" not in cmd.upper(): - return messages[0] + r = self.__send(cmd, delay) + messages = self.__protocol(r) # parses string into list of messages + + # select the first message with the ECU ID we're looking for + # TODO: use ELM header settings to query ECU by address directly + for message in messages: + if message.tx_id == self.__primary_ecu: + return message + + else: + debug("Rejected sending AT command") + return None - def send(self, cmd, delay=None): + + def __send(self, cmd, delay=None): + """ + unprotected send() function + + will __write() the given string, no questions asked. + returns result of __read() after an optional delay. + """ self.__write(cmd) @@ -197,13 +271,15 @@ def send(self, cmd, delay=None): r = self.__read() - return r # return raw string only + return r - # sends the hex string to the port def __write(self, cmd): + """ + "low-level" function to write a string to the port + """ - if self.__port: + if self.is_connected(): cmd += "\r\n" # terminate self.__port.flushOutput() self.__port.flushInput() @@ -213,9 +289,13 @@ def __write(self, cmd): debug("cannot perform write() when unconnected", True) - # accumulates until the prompt character is seen - # returns raw string def __read(self): + """ + "low-level" read function + + accumulates characters until the prompt character is seen + returns the raw string + """ attempts = 2 result = "" @@ -230,11 +310,11 @@ def __read(self): if attempts <= 0: break - debug("read() found nothing") + debug("__read() found nothing") attempts -= 1 continue - # end on chevron + # end on chevron (ELM prompt character) if c == ">": break @@ -248,58 +328,3 @@ def __read(self): debug("read: " + repr(result)) return result - - - # - # fixme: j1979 specifies that the program should poll until the number - # of returned DTCs matches the number indicated by a call to PID 01 - # - ''' - def get_dtc(self): - """Returns a list of all pending DTC codes. Each element consists of - a 2-tuple: (DTC code (string), Code description (string) )""" - dtcLetters = ["P", "C", "B", "U"] - r = self.sensor(1)[1] #data - dtcNumber = r[0] - mil = r[1] - DTCCodes = [] - - - print "Number of stored DTC:" + str(dtcNumber) + " MIL: " + str(mil) - # get all DTC, 3 per mesg response - for i in range(0, ((dtcNumber+2)/3)): - self.write(GET_DTC_COMMAND) - res = self.read() - print "DTC result:" + res - for i in range(0, 3): - val1 = unhex(res[3+i*6:5+i*6]) - val2 = unhex(res[6+i*6:8+i*6]) #get DTC codes from response (3 DTC each 2 bytes) - val = (val1<<8)+val2 #DTC val as int - - if val==0: #skip fill of last packet - break - - DTCStr=dtcLetters[(val&0xC000)>14]+str((val&0x3000)>>12)+str((val&0x0f00)>>8)+str((val&0x00f0)>>4)+str(val&0x000f) - DTCCodes.append(["Active",DTCStr]) - - #read mode 7 - self.write(GET_FREEZE_DTC_COMMAND) - res = self.read() - - if res[:7] == "NODATA": #no freeze frame - return DTCCodes - - print "DTC freeze result:" + res - for i in range(0, 3): - val1 = unhex(res[3+i*6:5+i*6]) - val2 = unhex(res[6+i*6:8+i*6]) #get DTC codes from response (3 DTC each 2 bytes) - val = (val1<<8)+val2 #DTC val as int - - if val==0: #skip fill of last packet - break - - DTCStr=dtcLetters[(val&0xC000)>14]+str((val&0x3000)>>12)+str((val&0x0f00)>>8)+str((val&0x00f0)>>4)+str(val&0x000f) - DTCCodes.append(["Passive",DTCStr]) - - return DTCCodes - ''' diff --git a/obd/obd.py b/obd/obd.py index ae41e53d..22fb708a 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -94,7 +94,11 @@ def get_port_name(self): def load_commands(self): - """ queries for available PIDs, sets their support status, and compiles a list of command objects """ + """ + queries for available PIDs, + sets their support status, + and compiles a list of command objects + """ debug("querying for supported PIDs (commands)...") @@ -103,8 +107,8 @@ def load_commands(self): pid_getters = commands.pid_getters() for get in pid_getters: - # GET commands should sequentialy turn themselves on (become marked as supported) - # MODE 1 PID 0 is marked supported by default + # PID listing commands should sequentialy become supported + # Mode 1 PID 0 is assumed to always be supported if not self.supports(get): continue @@ -151,15 +155,20 @@ def send(self, c): debug("Sending command: %s" % str(c)) - c_str = c.get_command() - m = self.port.send_and_parse(c_str) # send command and retrieve message - r = c.compute(m) # compute a response object + # send command and retrieve message + m = self.port.send_and_parse(c.get_command()) - return r + if m is None: + return Response() # return empty response + else: + return c.compute(m) # compute a response object def query(self, c, force=False): - """ facade 'send' command, protects against sending unsupported commands """ + """ + facade 'send' command function + protects against sending unsupported commands. + """ # check that the command is supported if not (self.supports(c) or force): diff --git a/obd/utils.py b/obd/utils.py index 9387486f..ca9c0185 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -100,6 +100,15 @@ def ascii_to_bytes(a): def strip(): return "".join(s.split()) +def numBitsSet(n): + # TODO: there must be a better way to do this... + total = 0 + ref = 1 + for b in range(8): + total += int(bool(n & ref)) + ref = ref << 1 + return total + def unhex(_hex): _hex = "0" if _hex == "" else _hex return int(_hex, 16) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index d95de759..44e8351e 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -29,12 +29,12 @@ def test_query(): def write(cmd): toCar[0] = cmd - o.is_connected = lambda *args: True - o.port.is_connected = lambda *args: True - - o.port._ELM327__protocol = SAE_J1850_PWM() - o.port._ELM327__write = write - o.port._ELM327__read = lambda *args: fromCar + o.is_connected = lambda *args: True + o.port.is_connected = lambda *args: True + o.port._ELM327__protocol = SAE_J1850_PWM() + o.port._ELM327__primary_ecu = 0x10 + o.port._ELM327__write = write + o.port._ELM327__read = lambda *args: fromCar # make sure unsupported commands don't write ------------------------------ fromCar = "48 6B 10 41 23 AB CD 10\r\r" @@ -46,67 +46,51 @@ def write(cmd): fromCar = "48 6B 10 41 23 AB CD 10\r\r" # preset the response r = o.query(cmd, force=True) # run assert toCar[0] == "0123" # verify that the command was sent correctly - assert r.raw_data == fromCar # verify that raw_data was stored in the Response assert r.value == "ABCD" # verify that the response was parsed correctly # response of greater length ---------------------------------------------- fromCar = "48 6B 10 41 23 AB CD EF 10\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" - assert r.raw_data == fromCar assert r.value == "ABCD" # response of lesser length ----------------------------------------------- fromCar = "48 6B 10 41 23 AB 10\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" - assert r.raw_data == fromCar assert r.value == "AB00" # NO DATA response -------------------------------------------------------- fromCar = "NO DATA" r = o.query(cmd, force=True) - assert r.raw_data == fromCar assert r.is_null() # malformed response ------------------------------------------------------ fromCar = "totaly not hex!@#$" r = o.query(cmd, force=True) - assert r.raw_data == fromCar assert r.is_null() # no response ------------------------------------------------------------- fromCar = "" r = o.query(cmd, force=True) - assert r.raw_data == fromCar assert r.is_null() - # accept responses from other ECUs (when single response) ------------------------------------------------------- + # reject responses from other ECUs --------------------------------------- fromCar = "48 6B 12 41 23 AB CD 10\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" - assert r.raw_data == fromCar - assert r.value == "ABCD" - - # disregard responses from other ECUs (when multiple responses)------------------------------------- - fromCar = "48 6B 12 41 23 AB CD 10\r\r48 6B 12 41 23 AB CD 10\r\r" - r = o.query(cmd, force=True) - assert toCar[0] == "0123" - assert r.raw_data == fromCar assert r.is_null() - # filter for ECU 10 ------------------------------------------------------- + # filter for primary ECU -------------------------------------------------- fromCar = "48 6B 12 41 23 AB CD 10\r\r 48 6B 10 41 23 AB CD 10\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" - assert r.raw_data == fromCar assert r.value == "ABCD" # ignore multiline responses ---------------------------------------------- fromCar = "48 6B 10 41 23 AB CD 10\r\r 48 6B 10 41 23 AB CD 10\r\r" r = o.query(cmd, force=True) assert toCar[0] == "0123" - assert r.raw_data == fromCar assert r.is_null() def test_load_commands(): From 6122ec966c5505ff3317b16fa42cb169734eaf2c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 15 Feb 2015 22:31:57 -0500 Subject: [PATCH 172/569] fixed syntax errors, removed connection checks from private functions to prevent errors while connecting --- obd/elm327.py | 82 +++++++++++++++++++++++++-------------------------- obd/utils.py | 2 +- 2 files changed, 41 insertions(+), 43 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index c5abd0f4..ab16843f 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -129,7 +129,7 @@ def __init__(self, portname): # -------------- 0100 (first command, SEARCH protocols) -------------- - r0100 = self.__send("0100", delay=1) # give it a second to search + r0100 = self.__send("0100", delay=3) # give it a second (or three) to search # ------------------- ATDPN (list protocol number) ------------------- @@ -138,11 +138,11 @@ def __init__(self, portname): # suppress any "automatic" prefix r = r[1:] if (len(r) > 1 and r.startswith("A")) else r - if r not in _SUPPORTED_PROTOCOLS: + if r not in self._SUPPORTED_PROTOCOLS: self.__error("ELM responded with unknown protocol") return - self.__protocol = _SUPPORTED_PROTOCOLS[r]() + self.__protocol = self._SUPPORTED_PROTOCOLS[r]() # Now that a protocol has been selected, we can figure out @@ -159,7 +159,7 @@ def __init__(self, portname): self.__connected = True - def __find_primary_ecu(messages): + def __find_primary_ecu(self, messages): """ Given a list of messages from different ECUS, (in response to the 0100 PID listing command) @@ -239,21 +239,25 @@ def send_and_parse(self, cmd, delay=None): if no appropriate response was recieved. """ - if "AT" not in cmd.upper(): + if not self.is_connected(): + debug("cannot send_and_parse() when unconnected", True) + return None - r = self.__send(cmd, delay) + if "AT" in cmd.upper(): + debug("Rejected sending AT command", True) + return None - messages = self.__protocol(r) # parses string into list of messages + r = self.__send(cmd, delay) - # select the first message with the ECU ID we're looking for - # TODO: use ELM header settings to query ECU by address directly - for message in messages: - if message.tx_id == self.__primary_ecu: - return message + messages = self.__protocol(r) # parses string into list of messages - else: - debug("Rejected sending AT command") - return None + # select the first message with the ECU ID we're looking for + # TODO: use ELM header settings to query ECU by address directly + for message in messages: + if message.tx_id == self.__primary_ecu: + return message + + return None # no suitable response was returned def __send(self, cmd, delay=None): @@ -279,14 +283,11 @@ def __write(self, cmd): "low-level" function to write a string to the port """ - if self.is_connected(): - cmd += "\r\n" # terminate - self.__port.flushOutput() - self.__port.flushInput() - self.__port.write(cmd) - debug("write: " + repr(cmd)) - else: - debug("cannot perform write() when unconnected", True) + cmd += "\r\n" # terminate + self.__port.flushOutput() + self.__port.flushInput() + self.__port.write(cmd) + debug("write: " + repr(cmd)) def __read(self): @@ -300,31 +301,28 @@ def __read(self): attempts = 2 result = "" - if self.__port: - while True: - c = self.__port.read(1) + while True: + c = self.__port.read(1) - # if nothing was recieved - if not c: + # if nothing was recieved + if not c: - if attempts <= 0: - break + if attempts <= 0: + break - debug("__read() found nothing") - attempts -= 1 - continue + debug("__read() found nothing") + attempts -= 1 + continue - # end on chevron (ELM prompt character) - if c == ">": - break + # end on chevron (ELM prompt character) + if c == ">": + break - # skip null characters (ELM spec page 9) - if c == '\x00': - continue + # skip null characters (ELM spec page 9) + if c == '\x00': + continue - result += c # whatever is left must be part of the response - else: - debug("cannot perform read() when unconnected", True) + result += c # whatever is left must be part of the response debug("read: " + repr(result)) return result diff --git a/obd/utils.py b/obd/utils.py index ca9c0185..186898c0 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -97,7 +97,7 @@ def ascii_to_bytes(a): b.append(int(a[i:i+2], 16)) return b -def strip(): +def strip(s): return "".join(s.split()) def numBitsSet(n): From 814f48d5425007d0e7e0e80b01faaa6bef87a45d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 15 Feb 2015 22:41:07 -0500 Subject: [PATCH 173/569] reinstated port checks in proper form --- obd/elm327.py | 49 ++++++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index ab16843f..67826461 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -283,11 +283,14 @@ def __write(self, cmd): "low-level" function to write a string to the port """ - cmd += "\r\n" # terminate - self.__port.flushOutput() - self.__port.flushInput() - self.__port.write(cmd) - debug("write: " + repr(cmd)) + if self.__port: + cmd += "\r\n" # terminate + self.__port.flushOutput() + self.__port.flushInput() + self.__port.write(cmd) + debug("write: " + repr(cmd)) + else: + debug("cannot perform __write() when unconnected", True) def __read(self): @@ -301,28 +304,32 @@ def __read(self): attempts = 2 result = "" - while True: - c = self.__port.read(1) + if self.__port: + while True: + c = self.__port.read(1) - # if nothing was recieved - if not c: + # if nothing was recieved + if not c: - if attempts <= 0: - break + if attempts <= 0: + break - debug("__read() found nothing") - attempts -= 1 - continue + debug("__read() found nothing") + attempts -= 1 + continue - # end on chevron (ELM prompt character) - if c == ">": - break + # end on chevron (ELM prompt character) + if c == ">": + break - # skip null characters (ELM spec page 9) - if c == '\x00': - continue + # skip null characters (ELM spec page 9) + if c == '\x00': + continue - result += c # whatever is left must be part of the response + result += c # whatever is left must be part of the response + else: + debug("cannot perform __read() when unconnected", True) + return "" debug("read: " + repr(result)) return result From ce8bdd8140a858817a5c265d9bb3e01194ea3b6f Mon Sep 17 00:00:00 2001 From: chrispyduck Date: Mon, 16 Feb 2015 12:25:15 -0500 Subject: [PATCH 174/569] porting from python2 to python3 --- obd/OBDCommand.py | 10 +++++----- obd/__init__.py | 12 ++++++------ obd/async.py | 15 ++++++--------- obd/commands.py | 10 +++++----- obd/decoders.py | 6 +++--- obd/obd.py | 10 +++++----- obd/port.py | 4 ++-- obd/utils.py | 4 ++-- tests/test_commands.py | 2 +- 9 files changed, 35 insertions(+), 38 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 79f15509..cfde5a7e 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -1,7 +1,7 @@ import re -from utils import * -from debug import debug +from .utils import * +from .debug import debug @@ -46,17 +46,17 @@ def compute(self, _data): r = Response(_data) # split by lines, and remove empty lines - lines = filter(bool, re.split("[\r\n]", _data)) + lines = list(filter(bool, re.split("[\r\n]", _data))) # splits each line by spaces (each element should be a hex byte) lines = [line.split() for line in lines] # filter by minimum response length (number of space delimited chunks (bytes)) - lines = filter(lambda line: len(line) >= 7, lines) + lines = [line for line in lines if len(line) >= 7] if len(lines) > 1: # filter for ECU 10 (engine) - lines = filter(lambda line: line[2] == '10', lines) + lines = [line for line in lines if line[2] == '10'] # by now, we should have only one line. # Any more, and its a multiline response (which this library can't handle yet) diff --git a/obd/__init__.py b/obd/__init__.py index 518a5f21..cc2cbd09 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -30,9 +30,9 @@ __version__ = '0.3.0' -from obd import OBD -from OBDCommand import OBDCommand -from commands import commands -from utils import scanSerial, Unit -from debug import debug -from async import Async +from .obd import OBD +from .OBDCommand import OBDCommand +from .commands import commands +from .utils import scanSerial, Unit +from .debug import debug +from .async import Async diff --git a/obd/async.py b/obd/async.py index 8854c033..778f0872 100644 --- a/obd/async.py +++ b/obd/async.py @@ -28,16 +28,13 @@ # # ######################################################################## -import obd import time import threading -from utils import Response -from commands import OBDCommand -from debug import debug +from .utils import Response +from .debug import debug +from . import OBD - - -class Async(obd.OBD): +class Async(OBD): """ subclass representing an OBD-II connection """ def __init__(self, portstr=None): @@ -85,7 +82,7 @@ def watch(self, c, callback=None, force=False): return # new command being watched, store the command - if not self.commands.has_key(c): + if c not in self.commands: debug("Watching command: %s" % str(c)) self.commands[c] = Response() # give it an initial value self.callbacks[c] = [] # create an empty list @@ -130,7 +127,7 @@ def unwatch_all(self): def query(self, c): - if self.commands.has_key(c): + if c in self.commands: return self.commands[c] else: return Response() diff --git a/obd/commands.py b/obd/commands.py index 1cdfb229..1237c697 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -28,9 +28,9 @@ # # ######################################################################## -from OBDCommand import OBDCommand -from decoders import * -from debug import debug +from .OBDCommand import OBDCommand +from .decoders import * +from .debug import debug @@ -202,7 +202,7 @@ def __init__(self): def __getitem__(self, key): if isinstance(key, int): return self.modes[key] - elif isinstance(key, basestring): + elif isinstance(key, str): return self.__dict__[key] else: debug("OBD commands can only be retrieved by PID value or dict name", True) @@ -249,7 +249,7 @@ def has_command(self, c): # checks for existance of command by name def has_name(self, s): - if isinstance(s, basestring): + if isinstance(s, str): return s.isupper() and (s in self.__dict__.keys()) else: debug("has_name() only accepts string names for commands", True) diff --git a/obd/decoders.py b/obd/decoders.py index 8c97befd..76a61a4b 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -29,9 +29,9 @@ ######################################################################## import math -from utils import * -from codes import * -from debug import debug +from .utils import * +from .codes import * +from .debug import debug ''' All decoders take the form: diff --git a/obd/obd.py b/obd/obd.py index a0663811..4e8740d1 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -29,10 +29,10 @@ ######################################################################## import time -from port import OBDPort, State -from commands import commands -from utils import scanSerial, Response -from debug import debug +from .port import OBDPort, State +from .commands import commands +from .utils import scanSerial, Response +from .debug import debug @@ -135,7 +135,7 @@ def load_commands(self): def print_commands(self): for c in self.supported_commands: - print str(c) + print(str(c)) def supports(self, c): diff --git a/obd/port.py b/obd/port.py index ca43491e..bdfc313a 100644 --- a/obd/port.py +++ b/obd/port.py @@ -31,8 +31,8 @@ import serial import string import time -from utils import Response, unhex -from debug import debug +from .utils import Response, unhex +from .debug import debug class State(): diff --git a/obd/utils.py b/obd/utils.py index 9444391f..f716066a 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -34,7 +34,7 @@ import time import glob import sys -from debug import debug +from .debug import debug class Unit: @@ -87,7 +87,7 @@ def __init__(self, name, available, incomplete): def __str__(self): a = "Available" if self.available else "Unavailable" c = "Incomplete" if self.incomplete else "Complete" - return "Test %s: %s, %s" % (name, a, c) + return "Test %s: %s, %s" % (self.name, a, c) diff --git a/tests/test_commands.py b/tests/test_commands.py index 9f582e5a..e1cad593 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -28,7 +28,7 @@ def test_unique_names(): for cmds in obd.commands.modes: for cmd in cmds: - assert not names.has_key(cmd.name), "Two commands share the same name: %s" % cmd.name + assert not names.__contains__(cmd.name), "Two commands share the same name: %s" % cmd.name names[cmd.name] = True From 5ba24a32f1f74e2fc28c2ba0e1c3814a2dcfb6f1 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 19 Feb 2015 13:57:56 -0500 Subject: [PATCH 175/569] adding a few last-minute changes before the tests get overhauled --- obd/OBDCommand.py | 9 ++++-- obd/elm327.py | 1 + obd/obd.py | 3 +- obd/utils.py | 9 ++---- tests/test_OBD.py | 1 + tests/test_OBDCommand.py | 61 +++++++++++++++++++++++++--------------- tests/test_elm327.py | 26 +++++++++++++++++ 7 files changed, 76 insertions(+), 34 deletions(-) create mode 100644 tests/test_elm327.py diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 0722d304..148eedf2 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -61,10 +61,11 @@ def get_mode_int(self): def get_pid_int(self): return unhex(self.pid) - def compute(self, message): + def __call__(self, message): # create the response object with the raw data recieved - r = Response(message) + # and reference to original command + r = Response(self, message) # combine the bytes back into a hex string # TODO: rewrite decoders to handle raw byte arrays @@ -79,7 +80,9 @@ def compute(self, message): _data = constrainHex(_data, self.bytes) # decoded value into the response object - r.set(self.decode(_data)) + d = self.decode(_data) + r.value = d[0] + r.unit = d[1] return r diff --git a/obd/elm327.py b/obd/elm327.py index 82576065..39507016 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -184,6 +184,7 @@ def __find_primary_ecu(self, messages): for message in messages: bits = sum([numBitsSet(b) for b in message.data_bytes]) + if bits > best: best = bits tx_id = message.tx_id diff --git a/obd/obd.py b/obd/obd.py index 2bbf94ec..cdae1358 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -34,7 +34,6 @@ from .commands import commands from .utils import scanSerial, Response from .debug import debug -from .port import OBDPort, State from .commands import commands from .utils import scanSerial, Response from .debug import debug @@ -165,7 +164,7 @@ def send(self, c): if m is None: return Response() # return empty response else: - return c.compute(m) # compute a response object + return c(m) # compute a response object def query(self, c, force=False): diff --git a/obd/utils.py b/obd/utils.py index 69f825d8..e09d6e24 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -62,19 +62,16 @@ class Unit: class Response(): - def __init__(self, raw_data=None): + def __init__(self, command=None, raw_data=None): + self.command = command + self.raw_data = raw_data self.value = None self.unit = Unit.NONE - self.raw_data = raw_data self.time = time.time() def is_null(self): return (self.raw_data == None) or (self.value == None) - def set(self, decode): - self.value = decode[0] - self.unit = decode[1] - def __str__(self): return "%s %s" % (str(self.value), str(self.unit)) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 44e8351e..66406384 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -93,5 +93,6 @@ def write(cmd): assert toCar[0] == "0123" assert r.is_null() + def test_load_commands(): pass diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index 229b8c85..98f6c751 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -1,6 +1,8 @@ from obd.commands import OBDCommand from obd.decoders import noop +from obd.protocols import * +from obd.protocols.protocol import Message def test_constructor(): @@ -24,7 +26,7 @@ def test_constructor(): def test_clone(): # name description mode cmd bytes decoder - cmd = OBDCommand("Test", "example OBD command", "01", "23", 2, noop) + cmd = OBDCommand("", "", "01", "23", 2, noop) other = cmd.clone() assert cmd.name == other.name @@ -36,30 +38,43 @@ def test_clone(): assert cmd.supported == cmd.supported -# TODO: rewrite these for new commands accepting messages (rather than strings) -""" +def test_call(): + p = SAE_J1850_PWM() + m = p("48 6B 10 41 00 BE 1F B8 11 AA\r\r") # parse valid data into response object -def test_data_stripping(): - # name description mode cmd bytes decoder - cmd = OBDCommand("Test", "example OBD command", "01", "00", 2, noop) - r = cmd.compute("48 6B 10 41 00 01 01 10\r\n") - assert not r.is_null() - assert r.value == "0101" + # valid response size + cmd = OBDCommand("", "", "01", "23", 4, noop) + r = cmd(m[0]) + assert r.value == "BE1FB811" + # response too short (pad) + cmd = OBDCommand("", "", "01", "23", 5, noop) + r = cmd(m[0]) + assert r.value == "BE1FB81100" -def test_data_not_hex(): - # name description mode cmd bytes decoder - cmd = OBDCommand("Test", "example OBD command", "01", "00", 2, noop) - r = cmd.compute("48 6B 10 41 00 wx yz 10\r\n") - assert r.is_null() - + # response too long (clip) + cmd = OBDCommand("", "", "01", "23", 3, noop) + r = cmd(m[0]) + assert r.value == "BE1FB8" -def test_data_length(): - # name description mode cmd bytes decoder - cmd = OBDCommand("Test", "example OBD command", "01", "00", 2, noop) - r = cmd.compute("48 6B 10 41 00 01 23 45 10\r\n") - assert r.value == "0123" - r = cmd.compute("48 6B 10 41 00 01 10\r\n") - assert r.value == "0100" -""" \ No newline at end of file +def test_get_command(): + cmd = OBDCommand("", "", "01", "23", 4, noop) + assert cmd.get_command() == "0123" # simple concat of mode and PID + + +def test_get_mode_int(): + cmd = OBDCommand("", "", "01", "23", 4, noop) + assert cmd.get_mode_int() == 0x01 + + cmd = OBDCommand("", "", "", "23", 4, noop) + assert cmd.get_mode_int() == 0 + + +def test_get_pid_int(): + cmd = OBDCommand("", "", "01", "23", 4, noop) + assert cmd.get_pid_int() == 0x23 + + cmd = OBDCommand("", "", "01", "", 4, noop) + assert cmd.get_pid_int() == 0 + diff --git a/tests/test_elm327.py b/tests/test_elm327.py new file mode 100644 index 00000000..27a954ca --- /dev/null +++ b/tests/test_elm327.py @@ -0,0 +1,26 @@ + +from obd.protocols import SAE_J1850_PWM +from obd.elm327 import ELM327 + + +def test_find_primary_ecu(): + # parse from messages + + p = ELM327 + p._ELM327__protocol = SAE_J1850_PWM() + + # use primary ECU when multiple are present + m = p._ELM327__protocol("48 6B 10 41 00 BE 1F B8 11 AA\r\r 48 6B 12 41 00 BE 1F B8 11 AA\r\r") + assert p._ELM327__find_primary_ecu(p, m) == 0x10 + + # use lone responses regardless + m = p._ELM327__protocol("48 6B 12 41 00 BE 1F B8 11 AA\r\r") + assert p._ELM327__find_primary_ecu(p, m) == 0x12 + + # if primary ECU is not listed, use response with most PIDs supported + m = p._ELM327__protocol("48 6B 12 41 00 BE 1F B8 11 AA\r\r 48 6B 14 41 00 00 00 B8 11 AA\r\r ") + print(m[0].data_bytes) + assert p._ELM327__find_primary_ecu(p, m) == 0x12 + + # if no messages were received, no ECU could be determined + assert p._ELM327__find_primary_ecu(p, []) == None From d2675fd0294c3556c808bcc6c1591b4cb2360b9a Mon Sep 17 00:00:00 2001 From: chrispyduck Date: Thu, 19 Feb 2015 19:49:04 -0500 Subject: [PATCH 176/569] more tweaks for python v3 --- obd/elm327.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 39507016..5964fd40 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -65,7 +65,7 @@ class ELM327: #"C" : None, # user defined 2 } - def __init__(self, portname): + def __init__(self, portname, baudrate=38400): """Initializes port by resetting device and gettings supported PIDs. """ self.__connected = False @@ -79,7 +79,7 @@ def __init__(self, portname): try: self.__port = serial.Serial(portname, \ - baudrate = 38400, \ + baudrate = baudrate, \ parity = serial.PARITY_NONE, \ stopbits = 1, \ bytesize = 8, \ @@ -97,7 +97,7 @@ def __init__(self, portname): # ---------------------------- ATZ (reset) ---------------------------- try: - r = self.__send("ATZ", delay=1) # wait 1 second for ELM to initialize + self.__send("ATZ", delay=1) # wait 1 second for ELM to initialize # return data can be junk, so don't bother checking except serial.SerialException as e: self.__error(e) @@ -106,24 +106,21 @@ def __init__(self, portname): # -------------------------- ATE0 (echo OFF) -------------------------- r = self.__send("ATE0") - r = strip(r) - if not r.endswith("OK"): + if not self.__isok(r, expectEcho=True): self.__error("ATE0 did not return 'OK'") return # ------------------------- ATH1 (headers ON) ------------------------- r = self.__send("ATH1") - r = strip(r) - if r != 'OK': + if not self.__isok(r): self.__error("ATH1 did not return 'OK', or echoing is still ON") return # ----------------------- ATSP0 (protocol AUTO) ----------------------- r = self.__send("ATSP0") - r = strip(r) - if r != 'OK': + if not self.__isok(r): self.__error("ATSP0 did not return 'OK'") return @@ -136,7 +133,7 @@ def __init__(self, portname): r = self.__send("ATDPN") r = strip(r) # suppress any "automatic" prefix - r = r[1:] if (len(r) > 1 and r.startswith("A")) else r + r = r[1:] if (len(r) > 1 and r.startswith("A")) else r[:-1] if r not in self._SUPPORTED_PROTOCOLS: self.__error("ELM responded with unknown protocol") @@ -158,6 +155,17 @@ def __init__(self, portname): debug("Connection successful") self.__connected = True + def __isok(self, data, expectEcho=False): + if not data: + return False + lines = list(map(str.strip, filter(None, data.split('\r\n')))) + if len(lines) < 2: + return False + if expectEcho: + return len(lines) == 3 and lines[1] == 'OK' and lines[2] == '>' + else: + return len(lines) == 2 and lines[0] == 'OK' and lines[1] == '>' + def __find_primary_ecu(self, messages): """ @@ -288,7 +296,7 @@ def __write(self, cmd): cmd += "\r\n" # terminate self.__port.flushOutput() self.__port.flushInput() - self.__port.write(cmd) + self.__port.write(cmd.encode()) debug("write: " + repr(cmd)) else: debug("cannot perform __write() when unconnected", True) @@ -303,7 +311,7 @@ def __read(self): """ attempts = 2 - result = "" + buffer = b'' if self.__port: while True: @@ -327,10 +335,10 @@ def __read(self): if c == '\x00': continue - result += c # whatever is left must be part of the response + buffer += c # whatever is left must be part of the response else: debug("cannot perform __read() when unconnected", True) return "" - debug("read: " + repr(result)) - return result + debug("read: " + repr(buffer)) + return buffer.decode() From d0cb3219ef8db98009d771c92028963835662067 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 19 Feb 2015 23:05:09 -0500 Subject: [PATCH 177/569] switched to ATSPA8 for auto protocol detection --- obd/elm327.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 39507016..522c7383 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -120,14 +120,13 @@ def __init__(self, portname): return - # ----------------------- ATSP0 (protocol AUTO) ----------------------- - r = self.__send("ATSP0") + # ----------------------- ATSPA8 (protocol AUTO) ----------------------- + r = self.__send("ATSPA8") r = strip(r) if r != 'OK': self.__error("ATSP0 did not return 'OK'") return - # -------------- 0100 (first command, SEARCH protocols) -------------- r0100 = self.__send("0100", delay=3) # give it a second (or three) to search From aeaa3bd5ec337ac6cda4aa7c0170dac5c7c8820e Mon Sep 17 00:00:00 2001 From: chrispyduck Date: Sat, 21 Feb 2015 07:42:40 -0500 Subject: [PATCH 178/569] allow configurable baudrate --- obd/obd.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/obd/obd.py b/obd/obd.py index cdae1358..866f9cbc 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -43,16 +43,16 @@ class OBD(object): """ class representing an OBD-II connection with it's assorted sensors """ - def __init__(self, portstr=None): + def __init__(self, portstr=None, baudrate=38400): self.port = None self.supported_commands = [] debug("========================== Starting python-OBD ==========================") - self.connect(portstr) # initialize by connecting and loading sensors + self.connect(portstr, baudrate) # initialize by connecting and loading sensors debug("=========================================================================") - def connect(self, portstr=None): + def connect(self, portstr=None, baudrate=38400): """ attempts to instantiate an ELM327 object. Loads commands on success""" if portstr is None: @@ -61,15 +61,15 @@ def connect(self, portstr=None): debug("Available ports: " + str(portnames)) for port in portnames: - - self.port = ELM327(port) + debug("Attempting to use port: " + str(port)) + self.port = ELM327(port, baudrate=baudrate) if self.port.is_connected(): # success! stop searching for serial break else: debug("Explicit port defined") - self.port = ELM327(portstr) + self.port = ELM327(portstr, baudrate=baudrate) # if a connection was made, query for commands if self.is_connected(): From 2b9729f72d950090c139575a9e3c9c7f8e43adf8 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 24 Feb 2015 18:11:54 -0500 Subject: [PATCH 179/569] fixed python2 error, check for prompt char with byte literal, moved sanitization to __read --- obd/elm327.py | 49 +++++++++++++++++++++++++-------------- obd/protocols/protocol.py | 4 ---- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 5964fd40..4a44fd0b 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -29,10 +29,11 @@ # # ######################################################################## +import re import serial import time from .protocols import * -from .utils import strip, numBitsSet +from .utils import numBitsSet from .debug import debug @@ -126,20 +127,28 @@ def __init__(self, portname, baudrate=38400): # -------------- 0100 (first command, SEARCH protocols) -------------- + # TODO: rewrite this using a "wait for prompt character" + # rather than a fixed wait period r0100 = self.__send("0100", delay=3) # give it a second (or three) to search # ------------------- ATDPN (list protocol number) ------------------- r = self.__send("ATDPN") - r = strip(r) + + if not r: + self.__error("Describe protocol command didn't return ") + return + + p = r[0] + # suppress any "automatic" prefix - r = r[1:] if (len(r) > 1 and r.startswith("A")) else r[:-1] + p = p[1:] if (len(p) > 1 and p.startswith("A")) else p[:-1] - if r not in self._SUPPORTED_PROTOCOLS: + if p not in self._SUPPORTED_PROTOCOLS: self.__error("ELM responded with unknown protocol") return - self.__protocol = self._SUPPORTED_PROTOCOLS[r]() + self.__protocol = self._SUPPORTED_PROTOCOLS[p]() # Now that a protocol has been selected, we can figure out @@ -155,16 +164,13 @@ def __init__(self, portname, baudrate=38400): debug("Connection successful") self.__connected = True - def __isok(self, data, expectEcho=False): - if not data: - return False - lines = list(map(str.strip, filter(None, data.split('\r\n')))) - if len(lines) < 2: + def __isok(self, lines, expectEcho=False): + if not lines: return False if expectEcho: - return len(lines) == 3 and lines[1] == 'OK' and lines[2] == '>' + return len(lines) == 2 and lines[1] == 'OK' else: - return len(lines) == 2 and lines[0] == 'OK' and lines[1] == '>' + return len(lines) == 1 and lines[0] == 'OK' def __find_primary_ecu(self, messages): @@ -296,7 +302,7 @@ def __write(self, cmd): cmd += "\r\n" # terminate self.__port.flushOutput() self.__port.flushInput() - self.__port.write(cmd.encode()) + self.__port.write(cmd.encode()) # turn the string into bytes debug("write: " + repr(cmd)) else: debug("cannot perform __write() when unconnected", True) @@ -307,7 +313,7 @@ def __read(self): "low-level" read function accumulates characters until the prompt character is seen - returns the raw string + returns a list of [/r/n] delimited strings """ attempts = 2 @@ -328,11 +334,11 @@ def __read(self): continue # end on chevron (ELM prompt character) - if c == ">": + if c == b'>': break # skip null characters (ELM spec page 9) - if c == '\x00': + if c == b'\x00': continue buffer += c # whatever is left must be part of the response @@ -341,4 +347,13 @@ def __read(self): return "" debug("read: " + repr(buffer)) - return buffer.decode() + + # convert bytes into a standard string + raw = buffer.decode() + + # splits into lines + # removes empty lines + # removes trailing spaces + lines = [ s.strip() for s in re.split("[\r\n]", raw) if bool(s) ] + + return lines diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index ee82b2ef..e7133fe7 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -29,7 +29,6 @@ # # ######################################################################## -import re from obd.utils import ascii_to_bytes, isHex from obd.debug import debug @@ -90,9 +89,6 @@ def __init__(self, baud=38400): def __call__(self, raw): - # split by lines into frames, and remove empty lines - lines = filter(bool, re.split("[\r\n]", raw)) - # ditch spaces lines = [line.replace(' ', '') for line in lines] From 9f2df625547094b63e05e2da1cc7ff2b592433c7 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 24 Feb 2015 18:33:29 -0500 Subject: [PATCH 180/569] added baudrate param to Async, removed old strip util --- obd/async.py | 4 ++-- obd/utils.py | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/obd/async.py b/obd/async.py index a11091f5..75cf6211 100644 --- a/obd/async.py +++ b/obd/async.py @@ -38,8 +38,8 @@ class Async(OBD): """ subclass representing an OBD-II connection """ - def __init__(self, portstr=None): - super(Async, self).__init__(portstr) + def __init__(self, portstr=None, baudrate=38400): + super(Async, self).__init__(portstr, baudrate) self.commands = {} # key = OBDCommand, value = Response self.callbacks = {} # key = OBDCommand, value = list of Functions self.thread = None diff --git a/obd/utils.py b/obd/utils.py index e09d6e24..259e0990 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -94,9 +94,6 @@ def ascii_to_bytes(a): b.append(int(a[i:i+2], 16)) return b -def strip(s): - return "".join(s.split()) - def numBitsSet(n): # TODO: there must be a better way to do this... total = 0 From d06684244d2cbe483e3b26d90ff6d7c9d214c362 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Mar 2015 19:09:15 -0500 Subject: [PATCH 181/569] Status command now outputs specialized object, rather than dict --- obd/commands.py | 2 +- obd/decoders.py | 35 +++++++++++++++++------------------ obd/obd.py | 2 +- obd/utils.py | 8 ++++++++ 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index ac703e13..c3ec5c64 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -160,7 +160,7 @@ __mode3__ = [ # sensor name description mode cmd bytes decoder - OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 6, noop ), + OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 6, noop , True), ] __mode4__ = [ diff --git a/obd/decoders.py b/obd/decoders.py index 364bf8d2..d567149c 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -218,27 +218,26 @@ def fuel_rate(_hex): def status(_hex): bits = bitstring(_hex, 32) - output = {} - output["Check_Engine_Light"] = bitToBool(bits[0]) - output["DTC_Count"] = unbin(bits[1:8]) - output["Ignition_Type"] = IGNITION_TYPE[unbin(bits[12])] - output["Tests"] = [] + output = Status() + output.MIL = bitToBool(bits[0]) + output.DTC_count = unbin(bits[1:8]) + output.ignition_type = IGNITION_TYPE[unbin(bits[12])] - output["Tests"].append(Test("Misfire", \ - bitToBool(bits[15]), \ - bitToBool(bits[11]))) + output.tests.append(Test("Misfire", \ + bitToBool(bits[15]), \ + bitToBool(bits[11]))) - output["Tests"].append(Test("Fuel System", \ - bitToBool(bits[14]), \ - bitToBool(bits[10]))) + output.tests.append(Test("Fuel System", \ + bitToBool(bits[14]), \ + bitToBool(bits[10]))) - output["Tests"].append(Test("Components", \ - bitToBool(bits[13]), \ - bitToBool(bits[9]))) + output.tests.append(Test("Components", \ + bitToBool(bits[13]), \ + bitToBool(bits[9]))) # different tests for different ignition types - if(output["Ignition_Type"] == IGNITION_TYPE[0]): # spark + if(output.ignition_type == IGNITION_TYPE[0]): # spark for i in range(8): if SPARK_TESTS[i] is not None: @@ -246,9 +245,9 @@ def status(_hex): bitToBool(bits[(2 * 8) + i]), \ bitToBool(bits[(3 * 8) + i])) - output["Tests"].append(t) + output.tests.append(t) - elif(output["Ignition_Type"] == IGNITION_TYPE[1]): # compression + elif(output.ignition_type == IGNITION_TYPE[1]): # compression for i in range(8): if COMPRESSION_TESTS[i] is not None: @@ -256,7 +255,7 @@ def status(_hex): bitToBool(bits[(2 * 8) + i]), \ bitToBool(bits[(3 * 8) + i])) - output["Tests"].append(t) + output.tests.append(t) return (output, Unit.NONE) diff --git a/obd/obd.py b/obd/obd.py index 43dc3784..cf431158 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -184,7 +184,7 @@ def query(self, c, force=False): def query_DTC(self): """ read all DTCs """ - n = self.query(commands.STATUS).value['DTC Count']; + n = self.query(commands.STATUS).value.DTC_count; n = n if (n < 128) else 0 # if this number is over 128, it's invalid codes = []; diff --git a/obd/utils.py b/obd/utils.py index 259e0990..08133a1d 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -76,6 +76,14 @@ def __str__(self): return "%s %s" % (str(self.value), str(self.unit)) +class Status(): + def __init__(self): + self.MIL = False + self.DTC_count = 0 + self.ignition_type = "" + self.tests = [] + + class Test(): def __init__(self, name, available, incomplete): self.name = name From 4b9cf750e1c1d1890e89ad91367c1ed66e781715 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Mar 2015 19:55:55 -0500 Subject: [PATCH 182/569] added smarter ignoring of mode and pid in response --- obd/OBDCommand.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 148eedf2..28dcea36 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -67,10 +67,17 @@ def __call__(self, message): # and reference to original command r = Response(self, message) + # discard the header/echo of the given command + # 0101 ----> [41 01] 83 00 00 00 + # vs. + # 03 ----> [43] 01 04 80 03 41 23 + header_bytes_expected = len(self.get_command()) // 2 + # combine the bytes back into a hex string # TODO: rewrite decoders to handle raw byte arrays _data = "" - for b in message.data_bytes[2:]: + + for b in message.data_bytes[header_bytes_expected:]: h = hex(b)[2:].upper() h = "0" + h if len(h) < 2 else h _data += h From b2e43b35a78594fac1eccc837bfe5e058aee62b2 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Mar 2015 20:48:19 -0500 Subject: [PATCH 183/569] got query_DTC returning cleanly with obdsim --- obd/commands.py | 2 +- obd/decoders.py | 28 ++++++++++++++++++++-------- obd/elm327.py | 1 + obd/obd.py | 19 +++++++++++++++---- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index c3ec5c64..7df92690 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -160,7 +160,7 @@ __mode3__ = [ # sensor name description mode cmd bytes decoder - OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 6, noop , True), + OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 6, dtc_frame , True), ] __mode4__ = [ diff --git a/obd/decoders.py b/obd/decoders.py index d567149c..493af64b 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -343,24 +343,36 @@ def describeCode(code): ''' # converts 2 bytes of hex into a DTC code -def dtc(_hex): +def single_dtc(_hex): + + if _hex == "0000": + return None + dtc = "" - bits = bitstring(_hex[0]) + bits = bitstring(_hex[0], 4) dtc += ['P', 'C', 'B', 'U'][unbin(bits[0:2])] dtc += str(unbin(bits[2:4])) dtc += _hex[1:4] - return (dtc, Unit.NONE) + return dtc # converts a frame of 2-byte DTCs into a list of DTCs +# example input = 01 04 80 03 41 23 +# [DTC] [DTC] [DTC] def dtc_frame(_hex): codes = [] - for n in range(3): + for n in range(0, 12, 4): + dtc = single_dtc(_hex[n:n+4]) + + if dtc is not None: + + # pull a description if we have one + if dtc in DTC: + dtc += ": %s" % DTC[dtc] + else: + dtc += ": unknown error code" - start = 4 * n - end = start + 4 - - codes.append(dtc(_hex[start:end])) + codes.append(dtc) return (codes, Unit.NONE) diff --git a/obd/elm327.py b/obd/elm327.py index eb54372e..0f993636 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -287,6 +287,7 @@ def __send(self, cmd, delay=None): self.__write(cmd) if delay is not None: + debug("wait: %d seconds" % delay) time.sleep(delay) return self.__read() diff --git a/obd/obd.py b/obd/obd.py index cf431158..6e1521b7 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -184,14 +184,25 @@ def query(self, c, force=False): def query_DTC(self): """ read all DTCs """ - n = self.query(commands.STATUS).value.DTC_count; + status = self.query(commands.STATUS); + + if status.is_null(): + debug("Failed to retrieve number of DTCs", True) + return [] + + n = status.value.DTC_count n = n if (n < 128) else 0 # if this number is over 128, it's invalid codes = []; while n > 0: - current_codes = self.query(commands.GET_DTC).value - codes += current_codes - n -= len(current_codes) + get_dtc = self.query(commands.GET_DTC) + + if get_dtc.is_null(): + debug("Failed to retrieve DTCs", True) + break + + codes += get_dtc.value + n -= len(get_dtc.value) return codes From a7937d9609acb51d294ab15763fa5726fee9b396 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 2 Mar 2015 22:54:42 -0500 Subject: [PATCH 184/569] wrote legacy multiline handler, started updating tests --- obd/OBDCommand.py | 6 --- obd/protocols/protocol.py | 2 +- obd/protocols/protocol_can.py | 3 ++ obd/protocols/protocol_legacy.py | 72 +++++++++++++++++++++++++++++--- obd/utils.py | 6 +-- tests/test_protocols.py | 31 ++++++++------ 6 files changed, 92 insertions(+), 28 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 28dcea36..8bd1e1f4 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -66,12 +66,6 @@ def __call__(self, message): # create the response object with the raw data recieved # and reference to original command r = Response(self, message) - - # discard the header/echo of the given command - # 0101 ----> [41 01] 83 00 00 00 - # vs. - # 03 ----> [43] 01 04 80 03 41 23 - header_bytes_expected = len(self.get_command()) // 2 # combine the bytes back into a hex string # TODO: rewrite decoders to handle raw byte arrays diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 28592f7f..0e86645e 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -128,7 +128,7 @@ def create_frame(self, raw): """ override in subclass for each protocol - Function recieves the raw string data for a frame. + Function recieves a list of byte values for a frame. Function should return a Frame instance. If fatal errors were found, this function should return None (the Frame is dropped). diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index e9a0040e..60f0e07d 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -57,6 +57,9 @@ def create_frame(self, raw): # read header information if self.id_bits == 11: + # Ex. + # 00 00 07 E8 06 41 00 BE 7F B8 13 + frame.priority = raw_bytes[2] & 0x0F # always 7 frame.addr_mode = raw_bytes[3] & 0xF0 # 0xD0 = functional, 0xE0 = physical diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 99e339ec..0725550f 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -45,9 +45,16 @@ def create_frame(self, raw): raw_bytes = ascii_to_bytes(raw) if len(raw_bytes) < 5: + debug("Discarded frame for being too short") return None - frame.data_bytes = raw_bytes[3:-1] # exclude trailing checksum (handled by ELM adapter) + # Ex. + # [Header] [ Frame ] + # 48 6B 10 41 00 BE 7F B8 13 ck + # ck = checksum byte + + # exclude header and trailing checksum (handled by ELM adapter) + frame.data_bytes = raw_bytes[3:-1] # read header information frame.priority = raw_bytes[0] @@ -60,11 +67,66 @@ def create_message(self, frames, tx_id): message = Message(frames, tx_id) - if len(frames) == 1: - message.data_bytes = message.frames[0].data_bytes + # len(frames) will always be >= 1 (see the caller, protocol.py) + mode = frames[0].data_bytes[0] + + # test that all frames are responses to the same Mode (SID) + if len(frames) > 1: + if not all([mode == f.data_bytes[0] for f in frames[1:]]): + debug("Recieved frames from multiple commands") + return None + + # legacy protocols have different re-assembly + # procedures for different Modes + + if mode == 0x43: + # GET_DTC requests return frames with no PID or order bytes + # accumulate all of the data, minus the Mode bytes of each frame + + # Ex. + # [ Frame ] + # 48 6B 10 43 03 00 03 02 03 03 ck + # 48 6B 10 43 03 04 00 00 00 00 ck + # [ Data ] + + for f in frames: + message.data_bytes += f.data_bytes[1:] + else: - debug("Recieved multi-frame response. Can't parse those yet") - return None + if len(frames) == 1: + # return data, excluding the mode/pid bytes + + # Ex. + # [ Frame ] + # 48 6B 10 41 00 BE 7F B8 13 ck + # [ Data ] + + message.data_bytes = frames[0].data_bytes[2:] + + else: # len(frames) > 1: + # generic multiline requests carry an order byte + + # Ex. + # [ Frame ] + # 48 6B 10 49 02 01 00 00 00 31 ck + # 48 6B 10 49 02 02 44 34 47 50 ck + # 48 6B 10 49 02 03 30 30 52 35 ck + # etc... [] [ Data ] + + # sort the frames by the order byte + frames = sorted(frames, key=lambda f: f[2]) + + # ensure that each order byte is consecutive by looking at + # them in pairs. (see if anything's missing) + indices = [f[2] for f in frames] + pairs = zip(indices, indices[1:]) + if not all([p[0]+1 == p[1] for p in pairs]): + debug("Recieved multiline response with missing frames") + return None + + # now that they're in order, accumulate the data from each one + for f in frames: + message.data_bytes += f.data_bytes[3:] return message diff --git a/obd/utils.py b/obd/utils.py index 08133a1d..771a9e51 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -62,15 +62,15 @@ class Unit: class Response(): - def __init__(self, command=None, raw_data=None): + def __init__(self, command=None, message=None): self.command = command - self.raw_data = raw_data + self.message = message self.value = None self.unit = Unit.NONE self.time = time.time() def is_null(self): - return (self.raw_data == None) or (self.value == None) + return (self.message == None) or (self.value == None) def __str__(self): return "%s %s" % (str(self.value), str(self.unit)) diff --git a/tests/test_protocols.py b/tests/test_protocols.py index 4396ffc9..52e6e6f4 100644 --- a/tests/test_protocols.py +++ b/tests/test_protocols.py @@ -36,36 +36,41 @@ def test_legacy(): # single frame cases - r = p("48 6B 10 41 00 BE 1F B8 11 AA\r\r") + r = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) assert len(r) == 1 - check_message(r[0], 1, 16, [65, 0, 190, 31, 184, 17]) + check_message(r[0], 1, 16, [190, 31, 184, 17]) - r = p("48 6B 10 41 00 BE 1F B8 11 AA") + r = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) assert len(r) == 1 - check_message(r[0], 1, 16, [65, 0, 190, 31, 184, 17]) + check_message(r[0], 1, 16, [190, 31, 184, 17]) - r = p("NO DATA") + r = p(["NO DATA"]) assert len(r) == 0 - r = p("TOTALLY NOT HEX") + r = p(["TOTALLY NOT HEX"]) assert len(r) == 0 # multi-frame cases # seperate ECUs, single frames each - r = p("48 6B 10 41 00 BE 1F B8 11 AA\r\r48 6B 11 41 00 01 02 03 04 AA\r\r") + r = p(["48 6B 10 41 00 BE 1F B8 11 AA", "48 6B 11 41 00 01 02 03 04 AA"]) assert len(r) == 2 - check_message(r[0], 1, 16, [65, 0, 190, 31, 184, 17]) - check_message(r[1], 1, 17, [65, 0, 1, 2, 3, 4 ]) + check_message(r[0], 1, 16, [190, 31, 184, 17]) + check_message(r[1], 1, 17, [1, 2, 3, 4 ]) - r = p("NO DATA\r\r48 6B 10 41 00 BE 1F B8 11 AA\r\r") + r = p(["NO DATA", "48 6B 10 41 00 BE 1F B8 11 AA"]) assert len(r) == 1 - check_message(r[0], 1, 16, [65, 0, 190, 31, 184, 17]) + check_message(r[0], 1, 16, [190, 31, 184, 17]) - r = p("NO DATA\r\rNO DATA\r\r") + r = p(["NO DATA", "NO DATA"]) assert len(r) == 0 + # GET_DTC requests + r = p(["48 6B 10 43 03 00 03 02 03 03 14", "48 6B 10 43 03 04 00 00 00 00 0D"]) + assert len(r) == 1 + check_message(r[0], 2, 16, [0x03, 0x0, 0x03, 0x02, 0x03, 0x03, 0x03, 0x04, 0x0, 0x0, 0x0, 0x0]) +''' def test_can_11(): for protocol in CAN_11_PROTOCOLS: p = protocol() @@ -102,7 +107,7 @@ def test_can_11(): r = p("NO DATA\r\rNO DATA\r\r") assert len(r) == 0 - +''' def test_can_29(): pass From 93e41f61d28d3d63c58edd9532d6ded92b5069f6 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 2 Mar 2015 23:12:36 -0500 Subject: [PATCH 185/569] updated rest of tests for __read that outputs lists. Fixed logic error with query(force) --- obd/OBDCommand.py | 2 +- obd/obd.py | 6 +++--- tests/test_OBD.py | 27 +++++++++++++++------------ tests/test_OBDCommand.py | 2 +- tests/test_elm327.py | 17 ++++++++--------- 5 files changed, 28 insertions(+), 26 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 8bd1e1f4..5f20f6d9 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -71,7 +71,7 @@ def __call__(self, message): # TODO: rewrite decoders to handle raw byte arrays _data = "" - for b in message.data_bytes[header_bytes_expected:]: + for b in message.data_bytes: h = hex(b)[2:].upper() h = "0" + h if len(h) < 2 else h _data += h diff --git a/obd/obd.py b/obd/obd.py index 6e1521b7..95ecc219 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -174,11 +174,11 @@ def query(self, c, force=False): """ # check that the command is supported - if not (self.supports(c) or force): + if self.supports(c) or force: + return self.send(c) + else: debug("'%s' is not supported" % str(c), True) return Response() # return empty response - else: - return self.send(c) def query_DTC(self): diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 66406384..90d70c11 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -18,7 +18,7 @@ def test_query(): # we don't need an actual serial connection o = obd.OBD("/dev/null") # forge our own command, to control the output - cmd = OBDCommand("TEST", "Test command", "01", "23", 2, noop) + cmd = OBDCommand("TEST", "Test command", "01", "23", 2, noop, False) # forge IO from the car by overwriting the read/write functions @@ -37,61 +37,64 @@ def write(cmd): o.port._ELM327__read = lambda *args: fromCar # make sure unsupported commands don't write ------------------------------ - fromCar = "48 6B 10 41 23 AB CD 10\r\r" + fromCar = ["48 6B 10 41 23 AB CD 10"] r = o.query(cmd) assert toCar[0] == "" assert r.is_null() # a correct command transaction ------------------------------------------- - fromCar = "48 6B 10 41 23 AB CD 10\r\r" # preset the response - r = o.query(cmd, force=True) # run + fromCar = ["48 6B 10 41 23 AB CD 10"] # preset the response + r = o.query(cmd, force=True) # run assert toCar[0] == "0123" # verify that the command was sent correctly + assert not r.is_null() assert r.value == "ABCD" # verify that the response was parsed correctly # response of greater length ---------------------------------------------- - fromCar = "48 6B 10 41 23 AB CD EF 10\r\r" + fromCar = ["48 6B 10 41 23 AB CD EF 10"] r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.value == "ABCD" # response of lesser length ----------------------------------------------- - fromCar = "48 6B 10 41 23 AB 10\r\r" + fromCar = ["48 6B 10 41 23 AB 10"] r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.value == "AB00" # NO DATA response -------------------------------------------------------- - fromCar = "NO DATA" + fromCar = ["NO DATA"] r = o.query(cmd, force=True) assert r.is_null() # malformed response ------------------------------------------------------ - fromCar = "totaly not hex!@#$" + fromCar = ["totaly not hex!@#$"] r = o.query(cmd, force=True) assert r.is_null() # no response ------------------------------------------------------------- - fromCar = "" + fromCar = [""] r = o.query(cmd, force=True) assert r.is_null() # reject responses from other ECUs --------------------------------------- - fromCar = "48 6B 12 41 23 AB CD 10\r\r" + fromCar = ["48 6B 12 41 23 AB CD 10"] r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.is_null() # filter for primary ECU -------------------------------------------------- - fromCar = "48 6B 12 41 23 AB CD 10\r\r 48 6B 10 41 23 AB CD 10\r\r" + fromCar = ["48 6B 12 41 23 AB CD 10", "48 6B 10 41 23 AB CD 10"] r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.value == "ABCD" + ''' # ignore multiline responses ---------------------------------------------- - fromCar = "48 6B 10 41 23 AB CD 10\r\r 48 6B 10 41 23 AB CD 10\r\r" + fromCar = ["48 6B 10 41 23 AB CD 10", "48 6B 10 41 23 AB CD 10"] r = o.query(cmd, force=True) assert toCar[0] == "0123" assert r.is_null() + ''' def test_load_commands(): diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index 98f6c751..d1788cb8 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -40,7 +40,7 @@ def test_clone(): def test_call(): p = SAE_J1850_PWM() - m = p("48 6B 10 41 00 BE 1F B8 11 AA\r\r") # parse valid data into response object + m = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) # parse valid data into response object # valid response size cmd = OBDCommand("", "", "01", "23", 4, noop) diff --git a/tests/test_elm327.py b/tests/test_elm327.py index 27a954ca..0264c18f 100644 --- a/tests/test_elm327.py +++ b/tests/test_elm327.py @@ -6,21 +6,20 @@ def test_find_primary_ecu(): # parse from messages - p = ELM327 + p = ELM327("/dev/null") # pyserial will yell, but this isn't testing tx/rx p._ELM327__protocol = SAE_J1850_PWM() # use primary ECU when multiple are present - m = p._ELM327__protocol("48 6B 10 41 00 BE 1F B8 11 AA\r\r 48 6B 12 41 00 BE 1F B8 11 AA\r\r") - assert p._ELM327__find_primary_ecu(p, m) == 0x10 + m = p._ELM327__protocol(["48 6B 10 41 00 BE 1F B8 11 AA", "48 6B 12 41 00 BE 1F B8 11 AA"]) + assert p._ELM327__find_primary_ecu(m) == 0x10 # use lone responses regardless - m = p._ELM327__protocol("48 6B 12 41 00 BE 1F B8 11 AA\r\r") - assert p._ELM327__find_primary_ecu(p, m) == 0x12 + m = p._ELM327__protocol(["48 6B 12 41 00 BE 1F B8 11 AA"]) + assert p._ELM327__find_primary_ecu(m) == 0x12 # if primary ECU is not listed, use response with most PIDs supported - m = p._ELM327__protocol("48 6B 12 41 00 BE 1F B8 11 AA\r\r 48 6B 14 41 00 00 00 B8 11 AA\r\r ") - print(m[0].data_bytes) - assert p._ELM327__find_primary_ecu(p, m) == 0x12 + m = p._ELM327__protocol(["48 6B 12 41 00 BE 1F B8 11 AA", "48 6B 14 41 00 00 00 B8 11 AA"]) + assert p._ELM327__find_primary_ecu(m) == 0x12 # if no messages were received, no ECU could be determined - assert p._ELM327__find_primary_ecu(p, []) == None + assert p._ELM327__find_primary_ecu([]) == None From a00281b5d099d2d22683a737bf050be69add7c01 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 3 Mar 2015 00:35:35 -0500 Subject: [PATCH 186/569] started writing CAN multi-frame handlers, updated CAN-11 tests --- obd/protocols/protocol.py | 4 +- obd/protocols/protocol_can.py | 74 +++++++++++++++++++++++++++++--- obd/protocols/protocol_legacy.py | 2 +- tests/test_protocols.py | 34 ++++++--------- 4 files changed, 83 insertions(+), 31 deletions(-) diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 0e86645e..e38f75f8 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -49,8 +49,8 @@ def __init__(self, raw): self.rx_id = None self.tx_id = None self.type = None - self.seq_id = 0 - self.msg_len = None + self.seq_index = 0 # only used when type = CF + self.data_len = None class Message(object): diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 60f0e07d..c28c900b 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -58,6 +58,7 @@ def create_frame(self, raw): # read header information if self.id_bits == 11: # Ex. + # [ ] # 00 00 07 E8 06 41 00 BE 7F B8 13 frame.priority = raw_bytes[2] & 0x0F # always 7 @@ -81,17 +82,32 @@ def create_frame(self, raw): frame.rx_id = raw_bytes[2] # 0x33 = broadcast (functional) frame.tx_id = raw_bytes[3] # 0xF1 = tester ID + # Ex. + # [ Frame ] + # 00 00 07 E8 06 41 00 BE 7F B8 13 frame.data_bytes = raw_bytes[4:] - # extra frame info in data section + # read PCI byte (always first byte in the data section) frame.type = frame.data_bytes[0] & 0xF0 - if frame.type not in [self.FRAME_TYPE_CF, - self.FRAME_TYPE_FF, - self.FRAME_TYPE_SF]: + if frame.type not in [self.FRAME_TYPE_SF, + self.FRAME_TYPE_FF, + self.FRAME_TYPE_CF]: + debug("Dropping frame carrying unknown PCI frame type") return None + if frame.type == self.FRAME_TYPE_SF: + # single frames have 4 bit length codes + frame.data_len = frame.data_bytes[0] & 0x0F + elif frame.type == self.FRAME_TYPE_FF: + # First frames have 12 bit length codes + frame.data_len = (frame.data_bytes[0] & 0x0F) << 8 + frame.data_len += frame.data_bytes[1] + elif frame.type == self.FRAME_TYPE_CF: + # Consecutive frames have 4 bit sequence indices + frame.seq_index = frame.data_bytes[0] & 0x0F + return frame @@ -100,10 +116,54 @@ def create_message(self, frames, tx_id): message = Message(frames, tx_id) if len(message.frames) == 1: - message.data_bytes = message.frames[0].data_bytes[1:] # ignore PCI byte + frame = frames[0] + + if frame.type != self.FRAME_TYPE_SF: + debug("Recieved lone frame not marked as single frame") + return None + + # extract data, ignore PCI byte and anything after the marked length + message.data_bytes = frame.data_bytes[1:1+frame.data_len] + else: - debug("Recieved multi-frame response. Can't parse those yet") - return None + # sort FF and CF into their own lists + + ff = [] + cf = [] + + for f in frames: + if f.type == self.FRAME_TYPE_FF: + ff.append(f) + elif f.type == self.FRAME_TYPE_CF: + cf.append(f) + else: + debug("Dropping frame in multi-frame response not marked as FF or CF") + + # check that we captured only one first-frame + if len(ff) > 1: + debug("Recieved multiple frames marked FF") + return None + elif len(ff) == 0: + debug("Never received frame marked FF") + return None + + # check that there was at least one consecutive-frame + if len(cf) == 0: + debug("Never received frame marked CF") + return None + + # TODO + + + + # chop off the Mode/PID bytes based on the mode number + mode = message.data_bytes[0] + if mode == 0x43: + # GET_DTC requests (mode 03) do not have a PID byte + message.data_bytes = message.data_bytes[1:] + else: + # handles cases when there is both a Mode and PID byte + message.data_bytes = message.data_bytes[2:] return message diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 0725550f..18a7c67a 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -45,7 +45,7 @@ def create_frame(self, raw): raw_bytes = ascii_to_bytes(raw) if len(raw_bytes) < 5: - debug("Discarded frame for being too short") + debug("Dropped frame for being too short") return None # Ex. diff --git a/tests/test_protocols.py b/tests/test_protocols.py index 52e6e6f4..f5dbae59 100644 --- a/tests/test_protocols.py +++ b/tests/test_protocols.py @@ -40,10 +40,6 @@ def test_legacy(): assert len(r) == 1 check_message(r[0], 1, 16, [190, 31, 184, 17]) - r = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) - assert len(r) == 1 - check_message(r[0], 1, 16, [190, 31, 184, 17]) - r = p(["NO DATA"]) assert len(r) == 0 @@ -70,44 +66,40 @@ def test_legacy(): assert len(r) == 1 check_message(r[0], 2, 16, [0x03, 0x0, 0x03, 0x02, 0x03, 0x03, 0x03, 0x04, 0x0, 0x0, 0x0, 0x0]) -''' + def test_can_11(): for protocol in CAN_11_PROTOCOLS: p = protocol() # single frame cases - r = p("7E8 06 41 00 BE 7F B8 13\r\r") + r = p(["7E8 06 41 00 BE 7F B8 13"]) assert len(r) == 1 - check_message(r[0], 1, 0, [65, 0, 190, 127, 184, 19]) + check_message(r[0], 1, 0, [190, 127, 184, 19]) - r = p("7E8 06 41 00 BE 7F B8 13") - assert len(r) == 1 - check_message(r[0], 1, 0, [65, 0, 190, 127, 184, 19]) - - r = p("NO DATA") + r = p(["NO DATA"]) assert len(r) == 0 - r = p("TOTALLY NOT HEX") + r = p(["TOTALLY NOT HEX"]) assert len(r) == 0 # multi-frame cases # seperate ECUs, single frames each - r = p("7E8 06 41 00 BE 7F B8 13 \r7EB 06 41 00 80 40 00 01 \r7EA 06 41 00 80 00 00 01 \r\r") + r = p(["7E8 06 41 00 BE 7F B8 13", "7EB 06 41 00 80 40 00 01", "7EA 06 41 00 80 00 00 01"]) assert len(r) == 3 # messages are returned in ECU order - check_message(r[0], 1, 0, [65, 0, 190, 127, 184, 19]) - check_message(r[1], 1, 2, [65, 0, 128, 0, 0, 1 ]) - check_message(r[2], 1, 3, [65, 0, 128, 64, 0, 1 ]) + check_message(r[0], 1, 0, [190, 127, 184, 19]) + check_message(r[1], 1, 2, [128, 0, 0, 1 ]) + check_message(r[2], 1, 3, [128, 64, 0, 1 ]) - r = p("NO DATA\r\r7E8 06 41 00 BE 7F B8 13\r\r") + r = p(["NO DATA", "7E8 06 41 00 BE 7F B8 13"]) assert len(r) == 1 - check_message(r[0], 1, 0, [65, 0, 190, 127, 184, 19]) + check_message(r[0], 1, 0, [190, 127, 184, 19]) - r = p("NO DATA\r\rNO DATA\r\r") + r = p(["NO DATA", "NO DATA"]) assert len(r) == 0 -''' + def test_can_29(): pass From ccdf9ffbb003a73e9b2de7806fa3962f76110636 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 17 Mar 2015 19:14:37 -0400 Subject: [PATCH 187/569] added frame sequence number processing, fixed frame ordering bug in legacy protocol --- obd/elm327.py | 2 +- obd/protocols/protocol_can.py | 29 ++++++++++++++++++++++++++++- obd/protocols/protocol_legacy.py | 4 ++-- tests/test_protocols.py | 9 +++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 0f993636..a0464cc1 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -188,7 +188,7 @@ def __find_primary_ecu(self, messages): # first, try filtering for the standard ECU IDs test = lambda m: m.tx_id == self.__protocol.PRIMARY_ECU - if bool(filter(test, messages)): + if bool([m for m in messages if test(m)]): return self.__protocol.PRIMARY_ECU else: # last resort solution, choose ECU diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index c28c900b..4cfed657 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -152,8 +152,35 @@ def create_message(self, frames, tx_id): debug("Never received frame marked CF") return None - # TODO + # calculate proper sequence indices from the lower 4 bits given + for prev, curr in zip(cf, cf[1:]): + # Frame sequence numbers only specify the low order bits, so compute the + # full sequence number from the frame number and the last sequence number seen: + # 1) take the high order bits from the last_sn and low order bits from the frame + seq = (prev.seq_index & ~0x0F) + (curr.seq_index) + # 2) if this is more than 7 frames away, we probably just wrapped (e.g., + # last=0x0F current=0x01 should mean 0x11, not 0x01) + if seq < prev.seq_index - 7: + # untested + seq += 0x10 + + curr.seq_index = seq + + # sort the sequence indices + cf = sorted(cf, key=lambda f: f.seq_index) + + # concat these lists together + frames = ff + cf + + # ensure that each order byte is consecutive by looking at + # them in pairs. (see if anything's missing) + indices = [f.seq_index for f in frames] + pairs = zip(indices, indices[1:]) + if not all([p[0]+1 == p[1] for p in pairs]): + debug("Recieved multiline response with missing frames") + return None + # TODO: extract message data # chop off the Mode/PID bytes based on the mode number diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 18a7c67a..0d33942a 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -114,11 +114,11 @@ def create_message(self, frames, tx_id): # etc... [] [ Data ] # sort the frames by the order byte - frames = sorted(frames, key=lambda f: f[2]) + frames = sorted(frames, key=lambda f: f.data_bytes[2]) # ensure that each order byte is consecutive by looking at # them in pairs. (see if anything's missing) - indices = [f[2] for f in frames] + indices = [f.data_bytes[2] for f in frames] pairs = zip(indices, indices[1:]) if not all([p[0]+1 == p[1] for p in pairs]): debug("Recieved multiline response with missing frames") diff --git a/tests/test_protocols.py b/tests/test_protocols.py index f5dbae59..42764aeb 100644 --- a/tests/test_protocols.py +++ b/tests/test_protocols.py @@ -100,6 +100,15 @@ def test_can_11(): r = p(["NO DATA", "NO DATA"]) assert len(r) == 0 + # multi-line response + ''' + r = p(["7E8 10 13 49 04 01 35 36 30", + "7E8 21 32 38 39 34 39 41 43", + "7E8 22 00 00 00 00 00 00 31" + ]) + assert len(r) == 1 + ''' + def test_can_29(): pass From 3bbc86a2935321793a6c10b6fb4acf0c9ee07178 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 22 Mar 2015 00:54:46 -0400 Subject: [PATCH 188/569] started writing tests for multi-line CAN stitching --- obd/protocols/protocol_can.py | 13 ++- ...test_protocols.py => test_protocol_can.py} | 101 ++++++++++-------- tests/test_protocol_legacy.py | 71 ++++++++++++ 3 files changed, 134 insertions(+), 51 deletions(-) rename tests/{test_protocols.py => test_protocol_can.py} (57%) create mode 100644 tests/test_protocol_legacy.py diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 4cfed657..2d991d0e 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -169,18 +169,21 @@ def create_message(self, frames, tx_id): # sort the sequence indices cf = sorted(cf, key=lambda f: f.seq_index) - # concat these lists together - frames = ff + cf - # ensure that each order byte is consecutive by looking at # them in pairs. (see if anything's missing) - indices = [f.seq_index for f in frames] + indices = [f.seq_index for f in cf] pairs = zip(indices, indices[1:]) if not all([p[0]+1 == p[1] for p in pairs]): debug("Recieved multiline response with missing frames") return None - # TODO: extract message data + # now that they're in order, load/accumulate the data from each frame + + # on the first frame, skip PCI byte AND length code + message.data_bytes += ff[0].data_bytes[2:] + + for f in cf: + message.data_bytes += f.data_bytes[1:] # chop off the PCI byte # chop off the Mode/PID bytes based on the mode number diff --git a/tests/test_protocols.py b/tests/test_protocol_can.py similarity index 57% rename from tests/test_protocols.py rename to tests/test_protocol_can.py index 42764aeb..849e0b1a 100644 --- a/tests/test_protocols.py +++ b/tests/test_protocol_can.py @@ -1,15 +1,11 @@ +import random from obd.protocols import * from obd.protocols.protocol import Message +from obd import debug +debug.console = True -LEGACY_PROTOCOLS = [ - SAE_J1850_PWM, - SAE_J1850_VPW, - ISO_9141_2, - ISO_14230_4_5baud, - ISO_14230_4_fast -] CAN_11_PROTOCOLS = [ ISO_15765_4_11bit_500k, @@ -30,15 +26,15 @@ def check_message(m, num_frames, tx_id, data_bytes): assert m.data_bytes == data_bytes -def test_legacy(): - for protocol in LEGACY_PROTOCOLS: +def test_can_11(): + for protocol in CAN_11_PROTOCOLS: p = protocol() # single frame cases - r = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) + r = p(["7E8 06 41 00 BE 7F B8 13"]) assert len(r) == 1 - check_message(r[0], 1, 16, [190, 31, 184, 17]) + check_message(r[0], 1, 0, [190, 127, 184, 19]) r = p(["NO DATA"]) assert len(r) == 0 @@ -46,61 +42,74 @@ def test_legacy(): r = p(["TOTALLY NOT HEX"]) assert len(r) == 0 - # multi-frame cases - # seperate ECUs, single frames each - r = p(["48 6B 10 41 00 BE 1F B8 11 AA", "48 6B 11 41 00 01 02 03 04 AA"]) - assert len(r) == 2 - check_message(r[0], 1, 16, [190, 31, 184, 17]) - check_message(r[1], 1, 17, [1, 2, 3, 4 ]) + r = p(["7E8 06 41 00 BE 7F B8 13", "7EB 06 41 00 80 40 00 01", "7EA 06 41 00 80 00 00 01"]) + assert len(r) == 3 + # messages are returned in ECU order + check_message(r[0], 1, 0, [190, 127, 184, 19]) + check_message(r[1], 1, 2, [128, 0, 0, 1 ]) + check_message(r[2], 1, 3, [128, 64, 0, 1 ]) - r = p(["NO DATA", "48 6B 10 41 00 BE 1F B8 11 AA"]) + r = p(["NO DATA", "7E8 06 41 00 BE 7F B8 13"]) assert len(r) == 1 - check_message(r[0], 1, 16, [190, 31, 184, 17]) + check_message(r[0], 1, 0, [190, 127, 184, 19]) r = p(["NO DATA", "NO DATA"]) assert len(r) == 0 - # GET_DTC requests - r = p(["48 6B 10 43 03 00 03 02 03 03 14", "48 6B 10 43 03 04 00 00 00 00 0D"]) - assert len(r) == 1 - check_message(r[0], 2, 16, [0x03, 0x0, 0x03, 0x02, 0x03, 0x03, 0x03, 0x04, 0x0, 0x0, 0x0, 0x0]) -def test_can_11(): - for protocol in CAN_11_PROTOCOLS: - p = protocol() + # MULTI-LINE STITCHING - # single frame cases + test_case = [ + "7E8 10 20 49 04 00 01 02 03", + "7E8 21 04 05 06 07 08 09 0A", + "7E8 22 0B 0C 0D 0E 0F 10 11", + "7E8 23 12 13 14 15 16 17 18" + ] - r = p(["7E8 06 41 00 BE 7F B8 13"]) + correct_data = list(range(25)) # range(25) = [00, 01, 02 ... 17, 18] + + # in-order + r = p(test_case) assert len(r) == 1 - check_message(r[0], 1, 0, [190, 127, 184, 19]) + check_message(r[0], len(test_case), 0, correct_data) - r = p(["NO DATA"]) - assert len(r) == 0 + # test a few out-of-order cases + for n in range(4): + random.shuffle(test_case) # mix up the frame strings + r = p(test_case) + assert len(r) == 1 + check_message(r[0], len(test_case), 0, correct_data) - r = p(["TOTALLY NOT HEX"]) - assert len(r) == 0 - # multi-frame cases - # seperate ECUs, single frames each - r = p(["7E8 06 41 00 BE 7F B8 13", "7EB 06 41 00 80 40 00 01", "7EA 06 41 00 80 00 00 01"]) - assert len(r) == 3 - # messages are returned in ECU order - check_message(r[0], 1, 0, [190, 127, 184, 19]) - check_message(r[1], 1, 2, [128, 0, 0, 1 ]) - check_message(r[2], 1, 3, [128, 64, 0, 1 ]) + # MODE 03 COMMANDS (GET_DTC) RETURN NO PID BYTE - r = p(["NO DATA", "7E8 06 41 00 BE 7F B8 13"]) + test_case = [ + "7E8 10 20 43 00 01 02 03 04", + "7E8 21 05 06 07 08 09 0A 0B", + ] + + correct_data = list(range(12)) # range(12) = [00, 01, 02 ... 0A, 0B] + + r = p(test_case) assert len(r) == 1 - check_message(r[0], 1, 0, [190, 127, 184, 19]) + check_message(r[0], len(test_case), 0, correct_data) + + + + ''' + # multi-line with shorter length + r = p(["7E8 10 14 01 02 03 04 05 06", + "7E8 21 07 08 09 0A 0B 0C 0D", + "7E8 22 0E 0F 10 11 12 13 14" + ]) + assert len(r) == 1 + check_message(r[0], 3, 0, list(range(2, 20))) + ''' - r = p(["NO DATA", "NO DATA"]) - assert len(r) == 0 - # multi-line response ''' r = p(["7E8 10 13 49 04 01 35 36 30", "7E8 21 32 38 39 34 39 41 43", diff --git a/tests/test_protocol_legacy.py b/tests/test_protocol_legacy.py new file mode 100644 index 00000000..2bb2b16b --- /dev/null +++ b/tests/test_protocol_legacy.py @@ -0,0 +1,71 @@ + +import random +from obd.protocols import * +from obd.protocols.protocol import Message + +from obd import debug +debug.console = True + + +LEGACY_PROTOCOLS = [ + SAE_J1850_PWM, + SAE_J1850_VPW, + ISO_9141_2, + ISO_14230_4_5baud, + ISO_14230_4_fast +] + + +def check_message(m, num_frames, tx_id, data_bytes): + """ generic test for correct message values """ + assert len(m.frames) == num_frames + assert m.tx_id == tx_id + assert m.data_bytes == data_bytes + + +def test_legacy(): + for protocol in LEGACY_PROTOCOLS: + p = protocol() + + # single frame cases + + r = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) + assert len(r) == 1 + check_message(r[0], 1, 16, [190, 31, 184, 17]) + + r = p(["NO DATA"]) + assert len(r) == 0 + + r = p(["TOTALLY NOT HEX"]) + assert len(r) == 0 + + # multi-frame cases + + # seperate ECUs, single frames each + r = p(["48 6B 10 41 00 BE 1F B8 11 AA", "48 6B 11 41 00 01 02 03 04 AA"]) + assert len(r) == 2 + check_message(r[0], 1, 16, [190, 31, 184, 17]) + check_message(r[1], 1, 17, [1, 2, 3, 4 ]) + + r = p(["NO DATA", "48 6B 10 41 00 BE 1F B8 11 AA"]) + assert len(r) == 1 + check_message(r[0], 1, 16, [190, 31, 184, 17]) + + r = p(["NO DATA", "NO DATA"]) + assert len(r) == 0 + + + + + # MODE 03 COMMANDS (GET_DTC) RETURN NO PID BYTE + + test_case = [ + "48 6B 10 43 00 01 02 03 04 05 FF", + "48 6B 10 43 06 07 08 09 0A 0B FF", + ] + + correct_data = list(range(12)) # data is stitched in order recieved + + r = p(test_case) + assert len(r) == 1 + check_message(r[0], len(test_case), 0x10, correct_data) From c94b8f372566b9a3f199bea614fd99cd13bfc18e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 22 Mar 2015 01:30:37 -0400 Subject: [PATCH 189/569] started functioning out protocol tests, fixed errors in contiguity checks --- obd/protocols/protocol_can.py | 8 ++- obd/protocols/protocol_legacy.py | 2 +- tests/test_protocol_can.py | 98 ++++++++++++++++++++------------ tests/test_protocol_legacy.py | 66 +++++++++++++++------ 4 files changed, 120 insertions(+), 54 deletions(-) diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 2d991d0e..0128847b 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -169,6 +169,12 @@ def create_message(self, frames, tx_id): # sort the sequence indices cf = sorted(cf, key=lambda f: f.seq_index) + # ensure that the last CF frame has the right + # index for the total frames recieved + if cf[-1].seq_index != len(cf): # not len() - 1, because FF is zero + debug("Recieved multiline response with incorrect frame count") + return None + # ensure that each order byte is consecutive by looking at # them in pairs. (see if anything's missing) indices = [f.seq_index for f in cf] @@ -193,7 +199,7 @@ def create_message(self, frames, tx_id): message.data_bytes = message.data_bytes[1:] else: # handles cases when there is both a Mode and PID byte - message.data_bytes = message.data_bytes[2:] + message.data_bytes = message.data_bytes[2:] return message diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 0d33942a..ce5d2ae0 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -44,7 +44,7 @@ def create_frame(self, raw): frame = Frame(raw) raw_bytes = ascii_to_bytes(raw) - if len(raw_bytes) < 5: + if len(raw_bytes) < 6: debug("Dropped frame for being too short") return None diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index 849e0b1a..0765710e 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -26,15 +26,24 @@ def check_message(m, num_frames, tx_id, data_bytes): assert m.data_bytes == data_bytes -def test_can_11(): + + + +def test_single_frame(): for protocol in CAN_11_PROTOCOLS: p = protocol() - # single frame cases - r = p(["7E8 06 41 00 BE 7F B8 13"]) + r = p(["7E8 06 41 00 00 01 02 03"]) assert len(r) == 1 - check_message(r[0], 1, 0, [190, 127, 184, 19]) + check_message(r[0], 1, 0x0, list(range(4))) + + + +def test_hex_straining(): + for protocol in CAN_11_PROTOCOLS: + p = protocol() + r = p(["NO DATA"]) assert len(r) == 0 @@ -42,24 +51,44 @@ def test_can_11(): r = p(["TOTALLY NOT HEX"]) assert len(r) == 0 + r = p(["NO DATA", "7E8 06 41 00 00 01 02 03"]) + assert len(r) == 1 + check_message(r[0], 1, 0x0, list(range(4))) + + r = p(["NO DATA", "NO DATA"]) + assert len(r) == 0 + + + +def test_multi_ecu(): + for protocol in CAN_11_PROTOCOLS: + p = protocol() + + + test_case = [ + "7E8 06 41 00 00 01 02 03", + "7EB 06 41 00 00 01 02 03", + "7EA 06 41 00 00 01 02 03", + ] + + correct_data = list(range(4)) + # seperate ECUs, single frames each - r = p(["7E8 06 41 00 BE 7F B8 13", "7EB 06 41 00 80 40 00 01", "7EA 06 41 00 80 00 00 01"]) + r = p(test_case) assert len(r) == 3 + # messages are returned in ECU order - check_message(r[0], 1, 0, [190, 127, 184, 19]) - check_message(r[1], 1, 2, [128, 0, 0, 1 ]) - check_message(r[2], 1, 3, [128, 64, 0, 1 ]) + check_message(r[0], 1, 0x0, correct_data) + check_message(r[1], 1, 0x2, correct_data) + check_message(r[2], 1, 0x3, correct_data) - r = p(["NO DATA", "7E8 06 41 00 BE 7F B8 13"]) - assert len(r) == 1 - check_message(r[0], 1, 0, [190, 127, 184, 19]) - r = p(["NO DATA", "NO DATA"]) - assert len(r) == 0 +def test_multi_line(): + for protocol in CAN_11_PROTOCOLS: + p = protocol() - # MULTI-LINE STITCHING test_case = [ "7E8 10 20 49 04 00 01 02 03", @@ -68,7 +97,7 @@ def test_can_11(): "7E8 23 12 13 14 15 16 17 18" ] - correct_data = list(range(25)) # range(25) = [00, 01, 02 ... 17, 18] + correct_data = list(range(25)) # in-order r = p(test_case) @@ -83,6 +112,23 @@ def test_can_11(): check_message(r[0], len(test_case), 0, correct_data) + # missing frames in a multi-frame message should drop the message + # (tests the contiguity check, and data length byte) + + test_case = [ + "7E8 10 20 49 04 00 01 02 03", + "7E8 21 04 05 06 07 08 09 0A", + "7E8 22 0B 0C 0D 0E 0F 10 11", + "7E8 23 12 13 14 15 16 17 18" + ] + + for n in range(len(test_case)): + sub_test = list(test_case) + del sub_test[n] + + r = p(sub_test) + assert len(r) == 0 + # MODE 03 COMMANDS (GET_DTC) RETURN NO PID BYTE @@ -91,7 +137,7 @@ def test_can_11(): "7E8 21 05 06 07 08 09 0A 0B", ] - correct_data = list(range(12)) # range(12) = [00, 01, 02 ... 0A, 0B] + correct_data = list(range(12)) r = p(test_case) assert len(r) == 1 @@ -99,25 +145,5 @@ def test_can_11(): - ''' - # multi-line with shorter length - r = p(["7E8 10 14 01 02 03 04 05 06", - "7E8 21 07 08 09 0A 0B 0C 0D", - "7E8 22 0E 0F 10 11 12 13 14" - ]) - assert len(r) == 1 - check_message(r[0], 3, 0, list(range(2, 20))) - ''' - - - ''' - r = p(["7E8 10 13 49 04 01 35 36 30", - "7E8 21 32 38 39 34 39 41 43", - "7E8 22 00 00 00 00 00 00 31" - ]) - assert len(r) == 1 - ''' - - def test_can_29(): pass diff --git a/tests/test_protocol_legacy.py b/tests/test_protocol_legacy.py index 2bb2b16b..9ed797a3 100644 --- a/tests/test_protocol_legacy.py +++ b/tests/test_protocol_legacy.py @@ -23,15 +23,28 @@ def check_message(m, num_frames, tx_id, data_bytes): assert m.data_bytes == data_bytes -def test_legacy(): +def test_single_frame(): for protocol in LEGACY_PROTOCOLS: p = protocol() - # single frame cases - - r = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) + # valid case + r = p(["48 6B 10 41 00 00 01 02 03 FF"]) assert len(r) == 1 - check_message(r[0], 1, 16, [190, 31, 184, 17]) + check_message(r[0], 1, 0x10, list(range(4))) + + # to short + r = p(["48 6B 10 41 FF"]) + assert len(r) == 0 + + # to long + # r = p(["48 6B 10 41 00 00 01 02 03 04 05 06 07 FF"]) + # assert len(r) == 0 + + +def test_hex_straining(): + for protocol in LEGACY_PROTOCOLS: + p = protocol() + r = p(["NO DATA"]) assert len(r) == 0 @@ -39,21 +52,42 @@ def test_legacy(): r = p(["TOTALLY NOT HEX"]) assert len(r) == 0 - # multi-frame cases - - # seperate ECUs, single frames each - r = p(["48 6B 10 41 00 BE 1F B8 11 AA", "48 6B 11 41 00 01 02 03 04 AA"]) - assert len(r) == 2 - check_message(r[0], 1, 16, [190, 31, 184, 17]) - check_message(r[1], 1, 17, [1, 2, 3, 4 ]) + r = p(["NO DATA", "NO DATA"]) + assert len(r) == 0 - r = p(["NO DATA", "48 6B 10 41 00 BE 1F B8 11 AA"]) + r = p(["NO DATA", "48 6B 10 41 00 00 01 02 03 FF"]) assert len(r) == 1 - check_message(r[0], 1, 16, [190, 31, 184, 17]) + check_message(r[0], 1, 0x10, list(range(4))) + + + +def test_multi_ecu(): + for protocol in LEGACY_PROTOCOLS: + p = protocol() + + + test_case = [ + "48 6B 10 41 00 00 01 02 03 FF", + "48 6B 11 41 00 00 01 02 03 FF", + "48 6B 12 41 00 00 01 02 03 FF", + ] + + correct_data = list(range(4)) + + r = p(test_case) + assert len(r) == len(test_case) + check_message(r[0], 1, 0x10, correct_data) + check_message(r[1], 1, 0x11, correct_data) + check_message(r[2], 1, 0x12, correct_data) + + + +def test_multi_line(): + for protocol in LEGACY_PROTOCOLS: + p = protocol() - r = p(["NO DATA", "NO DATA"]) - assert len(r) == 0 + # todo: normal response stitching From a614e0c66991a047d38aee0890d00e461beb23e3 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 22 Mar 2015 14:00:30 -0400 Subject: [PATCH 190/569] added util for contiguity check, added upper bounds for legacy frame length --- obd/protocols/protocol_can.py | 15 ++++----------- obd/protocols/protocol_legacy.py | 15 +++++++++------ obd/utils.py | 16 ++++++++++++++++ tests/test_protocol_can.py | 2 +- tests/test_protocol_legacy.py | 15 ++++++++++----- 5 files changed, 40 insertions(+), 23 deletions(-) diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 0128847b..05eafe46 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -29,6 +29,7 @@ # # ######################################################################## +from obd.utils import contiguous from .protocol import * @@ -169,25 +170,17 @@ def create_message(self, frames, tx_id): # sort the sequence indices cf = sorted(cf, key=lambda f: f.seq_index) - # ensure that the last CF frame has the right - # index for the total frames recieved - if cf[-1].seq_index != len(cf): # not len() - 1, because FF is zero - debug("Recieved multiline response with incorrect frame count") - return None - - # ensure that each order byte is consecutive by looking at - # them in pairs. (see if anything's missing) + # check contiguity indices = [f.seq_index for f in cf] - pairs = zip(indices, indices[1:]) - if not all([p[0]+1 == p[1] for p in pairs]): + if not contiguous(indices, 1, len(cf)): debug("Recieved multiline response with missing frames") return None - # now that they're in order, load/accumulate the data from each frame # on the first frame, skip PCI byte AND length code message.data_bytes += ff[0].data_bytes[2:] + # now that they're in order, load/accumulate the data from each CF frame for f in cf: message.data_bytes += f.data_bytes[1:] # chop off the PCI byte diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index ce5d2ae0..95fd4527 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -29,6 +29,7 @@ # # ######################################################################## +from obd.utils import contiguous from .protocol import * @@ -48,6 +49,10 @@ def create_frame(self, raw): debug("Dropped frame for being too short") return None + if len(raw_bytes) > 11: + debug("Dropped frame for being too long") + return None + # Ex. # [Header] [ Frame ] # 48 6B 10 41 00 BE 7F B8 13 ck @@ -116,17 +121,15 @@ def create_message(self, frames, tx_id): # sort the frames by the order byte frames = sorted(frames, key=lambda f: f.data_bytes[2]) - # ensure that each order byte is consecutive by looking at - # them in pairs. (see if anything's missing) + # check contiguity indices = [f.data_bytes[2] for f in frames] - pairs = zip(indices, indices[1:]) - if not all([p[0]+1 == p[1] for p in pairs]): + if not contiguous(indices, 1, len(frames)): debug("Recieved multiline response with missing frames") return None - # now that they're in order, accumulate the data from each one + # now that they're in order, accumulate the data from each frame for f in frames: - message.data_bytes += f.data_bytes[3:] + message.data_bytes += f.data_bytes[3:] # loose the mode/pid/seq bytes return message diff --git a/obd/utils.py b/obd/utils.py index 771a9e51..9194c506 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -149,6 +149,22 @@ def constrainHex(_hex, b): return _hex +# checks that a list of integers are consequtive +def contiguous(l, start, end): + if not l: + return False + if l[0] != start: + return False + if l[-1] != end: + return False + + # for consequtiveness, look at the integers in pairs + pairs = zip(l, l[1:]) + if not all([p[0]+1 == p[1] for p in pairs]): + return False + + return True + def try_port(portStr): """returns boolean for port availability""" diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index 0765710e..38bbf9c1 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -122,7 +122,7 @@ def test_multi_line(): "7E8 23 12 13 14 15 16 17 18" ] - for n in range(len(test_case)): + for n in range(len(test_case) - 1): sub_test = list(test_case) del sub_test[n] diff --git a/tests/test_protocol_legacy.py b/tests/test_protocol_legacy.py index 9ed797a3..da7daa0c 100644 --- a/tests/test_protocol_legacy.py +++ b/tests/test_protocol_legacy.py @@ -27,18 +27,23 @@ def test_single_frame(): for protocol in LEGACY_PROTOCOLS: p = protocol() - # valid case - r = p(["48 6B 10 41 00 00 01 02 03 FF"]) + # minimum valid length + r = p(["48 6B 10 41 00 FF"]) assert len(r) == 1 - check_message(r[0], 1, 0x10, list(range(4))) + check_message(r[0], 1, 0x10, []) + + # maximum valid length + r = p(["48 6B 10 41 00 00 01 02 03 04 FF"]) + assert len(r) == 1 + check_message(r[0], 1, 0x10, list(range(5))) # to short r = p(["48 6B 10 41 FF"]) assert len(r) == 0 # to long - # r = p(["48 6B 10 41 00 00 01 02 03 04 05 06 07 FF"]) - # assert len(r) == 0 + r = p(["48 6B 10 41 00 00 01 02 03 04 05 FF"]) + assert len(r) == 0 def test_hex_straining(): From 9bbc3ec9dc74a7ddda9860bd2c9654121a355c48 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 22 Mar 2015 14:09:48 -0400 Subject: [PATCH 191/569] added multi-line stitching tests for legacy protocol --- tests/test_protocol_can.py | 4 ++-- tests/test_protocol_legacy.py | 43 ++++++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index 38bbf9c1..7fc8065f 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -102,14 +102,14 @@ def test_multi_line(): # in-order r = p(test_case) assert len(r) == 1 - check_message(r[0], len(test_case), 0, correct_data) + check_message(r[0], len(test_case), 0x0, correct_data) # test a few out-of-order cases for n in range(4): random.shuffle(test_case) # mix up the frame strings r = p(test_case) assert len(r) == 1 - check_message(r[0], len(test_case), 0, correct_data) + check_message(r[0], len(test_case), 0x0, correct_data) # missing frames in a multi-frame message should drop the message diff --git a/tests/test_protocol_legacy.py b/tests/test_protocol_legacy.py index da7daa0c..f828967a 100644 --- a/tests/test_protocol_legacy.py +++ b/tests/test_protocol_legacy.py @@ -72,18 +72,21 @@ def test_multi_ecu(): test_case = [ + "48 6B 13 41 00 00 01 02 03 FF", "48 6B 10 41 00 00 01 02 03 FF", "48 6B 11 41 00 00 01 02 03 FF", - "48 6B 12 41 00 00 01 02 03 FF", ] correct_data = list(range(4)) + # seperate ECUs, single frames each r = p(test_case) assert len(r) == len(test_case) + + # messages are returned in ECU order check_message(r[0], 1, 0x10, correct_data) check_message(r[1], 1, 0x11, correct_data) - check_message(r[2], 1, 0x12, correct_data) + check_message(r[2], 1, 0x13, correct_data) @@ -92,8 +95,42 @@ def test_multi_line(): p = protocol() - # todo: normal response stitching + test_case = [ + "48 6B 10 49 02 01 00 01 02 03 FF", + "48 6B 10 49 02 02 04 05 06 07 FF", + "48 6B 10 49 02 03 08 09 0A 0B FF", + ] + + correct_data = list(range(12)) + + # in-order + r = p(test_case) + assert len(r) == 1 + check_message(r[0], len(test_case), 0x10, correct_data) + + # test a few out-of-order cases + for n in range(4): + random.shuffle(test_case) # mix up the frame strings + r = p(test_case) + assert len(r) == 1 + check_message(r[0], len(test_case), 0x10, correct_data) + + + # missing frames in a multi-frame message should drop the message + # (tests the contiguity check, and data length byte) + + test_case = [ + "48 6B 10 49 02 01 00 01 02 03 FF", + "48 6B 10 49 02 02 04 05 06 07 FF", + "48 6B 10 49 02 03 08 09 0A 0B FF", + ] + + for n in range(len(test_case) - 1): + sub_test = list(test_case) + del sub_test[n] + r = p(sub_test) + assert len(r) == 0 # MODE 03 COMMANDS (GET_DTC) RETURN NO PID BYTE From f7a68f5cb151182e241b2179bd2c6d86530d9167 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 22 Mar 2015 14:33:53 -0400 Subject: [PATCH 192/569] added tests for DTC decoders --- obd/commands.py | 6 +++--- obd/decoders.py | 20 +++++++++++--------- obd/obd.py | 3 --- tests/test_decoders.py | 23 +++++++++++++++++++++++ 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 7df92690..2eec6c88 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -160,17 +160,17 @@ __mode3__ = [ # sensor name description mode cmd bytes decoder - OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 6, dtc_frame , True), + OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 6, dtc , True), ] __mode4__ = [ # sensor name description mode cmd bytes decoder - OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", "" , 0, noop ), + OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", "" , 0, noop , True), ] __mode7__ = [ # sensor name description mode cmd bytes decoder - OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", "" , 0, noop ), + OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", "" , 0, noop , True), ] diff --git a/obd/decoders.py b/obd/decoders.py index 493af64b..ca60294c 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -345,12 +345,15 @@ def describeCode(code): # converts 2 bytes of hex into a DTC code def single_dtc(_hex): + if len(_hex) != 4: + return None + if _hex == "0000": return None - dtc = "" bits = bitstring(_hex[0], 4) + dtc = "" dtc += ['P', 'C', 'B', 'U'][unbin(bits[0:2])] dtc += str(unbin(bits[2:4])) dtc += _hex[1:4] @@ -358,21 +361,20 @@ def single_dtc(_hex): return dtc # converts a frame of 2-byte DTCs into a list of DTCs -# example input = 01 04 80 03 41 23 -# [DTC] [DTC] [DTC] -def dtc_frame(_hex): +# example input = "010480034123" +# [ ][ ][ ] +def dtc(_hex): codes = [] - for n in range(0, 12, 4): + for n in range(0, len(_hex), 4): dtc = single_dtc(_hex[n:n+4]) if dtc is not None: # pull a description if we have one + desc = "Unknown error code" if dtc in DTC: - dtc += ": %s" % DTC[dtc] - else: - dtc += ": unknown error code" + desc = DTC[dtc] - codes.append(dtc) + codes.append( (dtc, desc) ) return (codes, Unit.NONE) diff --git a/obd/obd.py b/obd/obd.py index 95ecc219..7fa01347 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -34,9 +34,6 @@ from .commands import commands from .utils import scanSerial, Response from .debug import debug -from .commands import commands -from .utils import scanSerial, Response -from .debug import debug diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 568768e4..d8fc9310 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -139,3 +139,26 @@ def test_air_status(): assert d.air_status("01") == ("Upstream", Unit.NONE) assert d.air_status("08") == ("Pump commanded on for diagnostics", Unit.NONE) assert d.air_status("03") == (None, Unit.NONE) + +def test_dtc(): + assert d.dtc("0104") == ([ + ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), + ], Unit.NONE) + + # multiple codes + assert d.dtc("010480034123") == ([ + ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), + ("B0003", "Unknown error code"), + ("C0123", "Unknown error code"), + ], Unit.NONE) + + # invalid code lengths are dropped + assert d.dtc("01048003412") == ([ + ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), + ("B0003", "Unknown error code"), + ], Unit.NONE) + + # 0000 codes are dropped + assert d.dtc("000001040000") == ([ + ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), + ], Unit.NONE) From 5ba08a16095c82d2e9ce4c4ed0fb46f1a4f36f4e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 22 Mar 2015 14:38:53 -0400 Subject: [PATCH 193/569] removed old describeCode decoder, minor formatting tweaks --- obd/decoders.py | 17 +---------------- obd/utils.py | 1 - 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index ca60294c..4c299b1a 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -304,6 +304,7 @@ def air_status(_hex): return (AIR_STATUS[i], Unit.NONE) + def obd_compliance(_hex): i = unhex(_hex) @@ -325,22 +326,6 @@ def fuel_type(_hex): return (v, Unit.NONE) -# Get the description of a DTC -def describeCode(code): - code.upper() - - v = "Unknown or manufacturer specific code. Consult the internet." - - if DTC.has_key(code): - v = DTC[code] - - return (v, Unit.NONE) - - - -''' -The following decoders are untested due to lack of a broken car -''' # converts 2 bytes of hex into a DTC code def single_dtc(_hex): diff --git a/obd/utils.py b/obd/utils.py index 9194c506..b18609f6 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -182,7 +182,6 @@ def try_port(portStr): return False - def scanSerial(): """scan for available ports. return a list of serial names""" available = [] From db167ed689d7c3fa63bb7130f82f198830235988 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 22 Mar 2015 15:18:49 -0400 Subject: [PATCH 194/569] turn off linefeeds, use DTC decoder for GET_FREEZE_DTC --- obd/commands.py | 2 +- obd/elm327.py | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 2eec6c88..493a3675 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -170,7 +170,7 @@ __mode7__ = [ # sensor name description mode cmd bytes decoder - OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", "" , 0, noop , True), + OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", "" , 0, dtc , True), ] diff --git a/obd/elm327.py b/obd/elm327.py index a0464cc1..bc979ba3 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -119,7 +119,14 @@ def __init__(self, portname, baudrate=38400): return - # ----------------------- ATSPA8 (protocol AUTO) ----------------------- + # ------------------------ ATL0 (linefeeds OFF) ----------------------- + r = self.__send("ATL0") + if not self.__isok(r): + self.__error("ATL0 did not return 'OK'") + return + + + # ---------------------- ATSPA8 (protocol AUTO) ----------------------- r = self.__send("ATSPA8") if not self.__isok(r): self.__error("ATSPA8 did not return 'OK'") @@ -148,9 +155,9 @@ def __init__(self, portname, baudrate=38400): self.__error("ELM responded with unknown protocol") return + # instantiate the correct protocol handler self.__protocol = self._SUPPORTED_PROTOCOLS[p]() - # Now that a protocol has been selected, we can figure out # which ECU is the primary. @@ -164,6 +171,7 @@ def __init__(self, portname, baudrate=38400): debug("Connection successful") self.__connected = True + def __isok(self, lines, expectEcho=False): if not lines: return False @@ -233,7 +241,7 @@ def close(self): Resets the device, and clears all attributes to unconnected state """ - if (self.__port != None) and self.__connected: + if self.is_connected(): self.__write("ATZ") self.__port.close() From b76e23d4ce3d3b75612db75b9fcce3e0fb819e15 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 22 Mar 2015 15:35:23 -0400 Subject: [PATCH 195/569] removed unnecessary query_DTC function --- obd/obd.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/obd/obd.py b/obd/obd.py index 7fa01347..8f5ac7b5 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -176,30 +176,3 @@ def query(self, c, force=False): else: debug("'%s' is not supported" % str(c), True) return Response() # return empty response - - - def query_DTC(self): - """ read all DTCs """ - - status = self.query(commands.STATUS); - - if status.is_null(): - debug("Failed to retrieve number of DTCs", True) - return [] - - n = status.value.DTC_count - n = n if (n < 128) else 0 # if this number is over 128, it's invalid - - codes = []; - - while n > 0: - get_dtc = self.query(commands.GET_DTC) - - if get_dtc.is_null(): - debug("Failed to retrieve DTCs", True) - break - - codes += get_dtc.value - n -= len(get_dtc.value) - - return codes From a9bb0e24bd9177372786c23321b0122fea3ebcd0 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 22 Mar 2015 16:13:34 -0400 Subject: [PATCH 196/569] got rid of the tabs --- obd/OBDCommand.py | 128 +- obd/async.py | 176 +- obd/codes.py | 4302 +++++++++++++++--------------- obd/commands.py | 404 +-- obd/debug.py | 20 +- obd/decoders.py | 328 +-- obd/elm327.py | 514 ++-- obd/obd.py | 198 +- obd/protocols/protocol_legacy.py | 164 +- obd/utils.py | 236 +- 10 files changed, 3235 insertions(+), 3235 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 5f20f6d9..4c20cbc0 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -35,67 +35,67 @@ class OBDCommand(): - def __init__(self, name, desc, mode, pid, returnBytes, decoder, supported=False): - self.name = name - self.desc = desc - self.mode = mode - self.pid = pid - self.bytes = returnBytes # number of bytes expected in return - self.decode = decoder - self.supported = supported - - def clone(self): - return OBDCommand(self.name, - self.desc, - self.mode, - self.pid, - self.bytes, - self.decode) - - def get_command(self): - return self.mode + self.pid # the actual command transmitted to the port - - def get_mode_int(self): - return unhex(self.mode) - - def get_pid_int(self): - return unhex(self.pid) - - def __call__(self, message): - - # create the response object with the raw data recieved - # and reference to original command - r = Response(self, message) - - # combine the bytes back into a hex string - # TODO: rewrite decoders to handle raw byte arrays - _data = "" - - for b in message.data_bytes: - h = hex(b)[2:].upper() - h = "0" + h if len(h) < 2 else h - _data += h - - # constrain number of bytes in response - if (self.bytes > 0): # zero bytes means flexible response - _data = constrainHex(_data, self.bytes) - - # decoded value into the response object - d = self.decode(_data) - r.value = d[0] - r.unit = d[1] - - return r - - def __str__(self): - return "%s%s: %s" % (self.mode, self.pid, self.desc) - - def __hash__(self): - # needed for using commands as keys in a dict (see async.py) - return hash((self.mode, self.pid)) - - def __eq__(self, other): - if isinstance(other, OBDCommand): - return (self.mode, self.pid) == (other.mode, other.pid) - else: - return False + def __init__(self, name, desc, mode, pid, returnBytes, decoder, supported=False): + self.name = name + self.desc = desc + self.mode = mode + self.pid = pid + self.bytes = returnBytes # number of bytes expected in return + self.decode = decoder + self.supported = supported + + def clone(self): + return OBDCommand(self.name, + self.desc, + self.mode, + self.pid, + self.bytes, + self.decode) + + def get_command(self): + return self.mode + self.pid # the actual command transmitted to the port + + def get_mode_int(self): + return unhex(self.mode) + + def get_pid_int(self): + return unhex(self.pid) + + def __call__(self, message): + + # create the response object with the raw data recieved + # and reference to original command + r = Response(self, message) + + # combine the bytes back into a hex string + # TODO: rewrite decoders to handle raw byte arrays + _data = "" + + for b in message.data_bytes: + h = hex(b)[2:].upper() + h = "0" + h if len(h) < 2 else h + _data += h + + # constrain number of bytes in response + if (self.bytes > 0): # zero bytes means flexible response + _data = constrainHex(_data, self.bytes) + + # decoded value into the response object + d = self.decode(_data) + r.value = d[0] + r.unit = d[1] + + return r + + def __str__(self): + return "%s%s: %s" % (self.mode, self.pid, self.desc) + + def __hash__(self): + # needed for using commands as keys in a dict (see async.py) + return hash((self.mode, self.pid)) + + def __eq__(self, other): + if isinstance(other, OBDCommand): + return (self.mode, self.pid) == (other.mode, other.pid) + else: + return False diff --git a/obd/async.py b/obd/async.py index 75cf6211..b5176ce7 100644 --- a/obd/async.py +++ b/obd/async.py @@ -36,121 +36,121 @@ from . import OBD class Async(OBD): - """ subclass representing an OBD-II connection """ + """ subclass representing an OBD-II connection """ - def __init__(self, portstr=None, baudrate=38400): - super(Async, self).__init__(portstr, baudrate) - self.commands = {} # key = OBDCommand, value = Response - self.callbacks = {} # key = OBDCommand, value = list of Functions - self.thread = None - self.running = False + def __init__(self, portstr=None, baudrate=38400): + super(Async, self).__init__(portstr, baudrate) + self.commands = {} # key = OBDCommand, value = Response + self.callbacks = {} # key = OBDCommand, value = list of Functions + self.thread = None + self.running = False - def start(self): - if self.is_connected(): - debug("Starting async thread") - self.running = True - self.thread = threading.Thread(target=self.run) - self.thread.daemon = True - self.thread.start() - else: - debug("Async thread not started because no connection was made") + def start(self): + if self.is_connected(): + debug("Starting async thread") + self.running = True + self.thread = threading.Thread(target=self.run) + self.thread.daemon = True + self.thread.start() + else: + debug("Async thread not started because no connection was made") - def stop(self): - if self.thread is not None: - debug("Stopping async thread...") - self.running = False - self.thread.join() - self.thread = None - debug("Async thread stopped") + def stop(self): + if self.thread is not None: + debug("Stopping async thread...") + self.running = False + self.thread.join() + self.thread = None + debug("Async thread stopped") - def close(self): - self.stop() - super(Async, self).close() + def close(self): + self.stop() + super(Async, self).close() - def watch(self, c, callback=None, force=False): + def watch(self, c, callback=None, force=False): - # the dict shouldn't be changed while the daemon thread is iterating - if self.running: - debug("Can't watch() while running, please use stop()", True) - else: + # the dict shouldn't be changed while the daemon thread is iterating + if self.running: + debug("Can't watch() while running, please use stop()", True) + else: - if not (self.supports(c) or force): - debug("'%s' is not supported" % str(c), True) - return + if not (self.supports(c) or force): + debug("'%s' is not supported" % str(c), True) + return - # new command being watched, store the command - if c not in self.commands: - debug("Watching command: %s" % str(c)) - self.commands[c] = Response() # give it an initial value - self.callbacks[c] = [] # create an empty list + # new command being watched, store the command + if c not in self.commands: + debug("Watching command: %s" % str(c)) + self.commands[c] = Response() # give it an initial value + self.callbacks[c] = [] # create an empty list - # if a callback was given, push it - if hasattr(callback, "__call__") and (callback not in self.callbacks[c]): - debug("subscribing callback for command: %s" % str(c)) - self.callbacks[c].append(callback) + # if a callback was given, push it + if hasattr(callback, "__call__") and (callback not in self.callbacks[c]): + debug("subscribing callback for command: %s" % str(c)) + self.callbacks[c].append(callback) - def unwatch(self, c, callback=None): + def unwatch(self, c, callback=None): - # the dict shouldn't be changed while the daemon thread is iterating - if self.running: - debug("Can't unwatch() while running, please use stop()", True) - else: - debug("Unwatching command: %s" % str(c)) + # the dict shouldn't be changed while the daemon thread is iterating + if self.running: + debug("Can't unwatch() while running, please use stop()", True) + else: + debug("Unwatching command: %s" % str(c)) - if c in self.commands: - # if a callback was specified, only remove the callback - if hasattr(callback, "__call__") and (callback in self.callbacks[c]): - self.callbacks[c].remove(callback) + if c in self.commands: + # if a callback was specified, only remove the callback + if hasattr(callback, "__call__") and (callback in self.callbacks[c]): + self.callbacks[c].remove(callback) - # if no more callbacks are left, remove the command entirely - if len(self.callbacks[c]) == 0: - self.commands.pop(c, None) - else: - # no callback was specified, pop everything - self.callbacks.pop(c, None) - self.commands.pop(c, None) + # if no more callbacks are left, remove the command entirely + if len(self.callbacks[c]) == 0: + self.commands.pop(c, None) + else: + # no callback was specified, pop everything + self.callbacks.pop(c, None) + self.commands.pop(c, None) - def unwatch_all(self): + def unwatch_all(self): - # the dict shouldn't be changed while the daemon thread is iterating - if self.running: - debug("Can't unwatch_all() while running, please use stop()", True) - else: - debug("Unwatching all") - self.commands = {} - self.callbacks = {} + # the dict shouldn't be changed while the daemon thread is iterating + if self.running: + debug("Can't unwatch_all() while running, please use stop()", True) + else: + debug("Unwatching all") + self.commands = {} + self.callbacks = {} - def query(self, c): - if c in self.commands: - return self.commands[c] - else: - return Response() + def query(self, c): + if c in self.commands: + return self.commands[c] + else: + return Response() - def run(self): - """ Daemon thread """ + def run(self): + """ Daemon thread """ - # loop until the stop signal is recieved - while self.running: + # loop until the stop signal is recieved + while self.running: - if len(self.commands) > 0: - # loop over the requested commands, send, and collect the response - for c in self.commands: - r = self.send(c) + if len(self.commands) > 0: + # loop over the requested commands, send, and collect the response + for c in self.commands: + r = self.send(c) - # store the response - self.commands[c] = r + # store the response + self.commands[c] = r - # fire the callbacks, if there are any - for callback in self.callbacks[c]: - callback(r) + # fire the callbacks, if there are any + for callback in self.callbacks[c]: + callback(r) - else: - time.sleep(1) # idle + else: + time.sleep(1) # idle diff --git a/obd/codes.py b/obd/codes.py index 44bb9e34..3079b87b 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -30,2178 +30,2178 @@ ######################################################################## DTC = { - "P0001": "Fuel Volume Regulator Control Circuit/Open", - "P0002": "Fuel Volume Regulator Control Circuit Range/Performance", - "P0003": "Fuel Volume Regulator Control Circuit Low", - "P0004": "Fuel Volume Regulator Control Circuit High", - "P0005": "Fuel Shutoff Valve 'A' Control Circuit/Open", - "P0006": "Fuel Shutoff Valve 'A' Control Circuit Low", - "P0007": "Fuel Shutoff Valve 'A' Control Circuit High", - "P0008": "Engine Position System Performance", - "P0009": "Engine Position System Performance", - "P0010": "'A' Camshaft Position Actuator Circuit", - "P0011": "'A' Camshaft Position - Timing Over-Advanced or System Performance", - "P0012": "'A' Camshaft Position - Timing Over-Retarded", - "P0013": "'B' Camshaft Position - Actuator Circuit", - "P0014": "'B' Camshaft Position - Timing Over-Advanced or System Performance", - "P0015": "'B' Camshaft Position - Timing Over-Retarded", - "P0016": "Crankshaft Position - Camshaft Position Correlation", - "P0017": "Crankshaft Position - Camshaft Position Correlation", - "P0018": "Crankshaft Position - Camshaft Position Correlation", - "P0019": "Crankshaft Position - Camshaft Position Correlation", - "P0020": "'A' Camshaft Position Actuator Circuit", - "P0021": "'A' Camshaft Position - Timing Over-Advanced or System Performance", - "P0022": "'A' Camshaft Position - Timing Over-Retarded", - "P0023": "'B' Camshaft Position - Actuator Circuit", - "P0024": "'B' Camshaft Position - Timing Over-Advanced or System Performance", - "P0025": "'B' Camshaft Position - Timing Over-Retarded", - "P0026": "Intake Valve Control Solenoid Circuit Range/Performance", - "P0027": "Exhaust Valve Control Solenoid Circuit Range/Performance", - "P0028": "Intake Valve Control Solenoid Circuit Range/Performance", - "P0029": "Exhaust Valve Control Solenoid Circuit Range/Performance", - "P0030": "HO2S Heater Control Circuit", - "P0031": "HO2S Heater Control Circuit Low", - "P0032": "HO2S Heater Control Circuit High", - "P0033": "Turbo Charger Bypass Valve Control Circuit", - "P0034": "Turbo Charger Bypass Valve Control Circuit Low", - "P0035": "Turbo Charger Bypass Valve Control Circuit High", - "P0036": "HO2S Heater Control Circuit", - "P0037": "HO2S Heater Control Circuit Low", - "P0038": "HO2S Heater Control Circuit High", - "P0039": "Turbo/Super Charger Bypass Valve Control Circuit Range/Performance", - "P0040": "O2 Sensor Signals Swapped Bank 1 Sensor 1/ Bank 2 Sensor 1", - "P0041": "O2 Sensor Signals Swapped Bank 1 Sensor 2/ Bank 2 Sensor 2", - "P0042": "HO2S Heater Control Circuit", - "P0043": "HO2S Heater Control Circuit Low", - "P0044": "HO2S Heater Control Circuit High", - "P0045": "Turbo/Super Charger Boost Control Solenoid Circuit/Open", - "P0046": "Turbo/Super Charger Boost Control Solenoid Circuit Range/Performance", - "P0047": "Turbo/Super Charger Boost Control Solenoid Circuit Low", - "P0048": "Turbo/Super Charger Boost Control Solenoid Circuit High", - "P0049": "Turbo/Super Charger Turbine Overspeed", - "P0050": "HO2S Heater Control Circuit", - "P0051": "HO2S Heater Control Circuit Low", - "P0052": "HO2S Heater Control Circuit High", - "P0053": "HO2S Heater Resistance", - "P0054": "HO2S Heater Resistance", - "P0055": "HO2S Heater Resistance", - "P0056": "HO2S Heater Control Circuit", - "P0057": "HO2S Heater Control Circuit Low", - "P0058": "HO2S Heater Control Circuit High", - "P0059": "HO2S Heater Resistance", - "P0060": "HO2S Heater Resistance", - "P0061": "HO2S Heater Resistance", - "P0062": "HO2S Heater Control Circuit", - "P0063": "HO2S Heater Control Circuit Low", - "P0064": "HO2S Heater Control Circuit High", - "P0065": "Air Assisted Injector Control Range/Performance", - "P0066": "Air Assisted Injector Control Circuit or Circuit Low", - "P0067": "Air Assisted Injector Control Circuit High", - "P0068": "MAP/MAF - Throttle Position Correlation", - "P0069": "Manifold Absolute Pressure - Barometric Pressure Correlation", - "P0070": "Ambient Air Temperature Sensor Circuit", - "P0071": "Ambient Air Temperature Sensor Range/Performance", - "P0072": "Ambient Air Temperature Sensor Circuit Low", - "P0073": "Ambient Air Temperature Sensor Circuit High", - "P0074": "Ambient Air Temperature Sensor Circuit Intermittent", - "P0075": "Intake Valve Control Solenoid Circuit", - "P0076": "Intake Valve Control Solenoid Circuit Low", - "P0077": "Intake Valve Control Solenoid Circuit High", - "P0078": "Exhaust Valve Control Solenoid Circuit", - "P0079": "Exhaust Valve Control Solenoid Circuit Low", - "P0080": "Exhaust Valve Control Solenoid Circuit High", - "P0081": "Intake Valve Control Solenoid Circuit", - "P0082": "Intake Valve Control Solenoid Circuit Low", - "P0083": "Intake Valve Control Solenoid Circuit High", - "P0084": "Exhaust Valve Control Solenoid Circuit", - "P0085": "Exhaust Valve Control Solenoid Circuit Low", - "P0086": "Exhaust Valve Control Solenoid Circuit High", - "P0087": "Fuel Rail/System Pressure - Too Low", - "P0088": "Fuel Rail/System Pressure - Too High", - "P0089": "Fuel Pressure Regulator 1 Performance", - "P0090": "Fuel Pressure Regulator 1 Control Circuit", - "P0091": "Fuel Pressure Regulator 1 Control Circuit Low", - "P0092": "Fuel Pressure Regulator 1 Control Circuit High", - "P0093": "Fuel System Leak Detected - Large Leak", - "P0094": "Fuel System Leak Detected - Small Leak", - "P0095": "Intake Air Temperature Sensor 2 Circuit", - "P0096": "Intake Air Temperature Sensor 2 Circuit Range/Performance", - "P0097": "Intake Air Temperature Sensor 2 Circuit Low", - "P0098": "Intake Air Temperature Sensor 2 Circuit High", - "P0099": "Intake Air Temperature Sensor 2 Circuit Intermittent/Erratic", - "P0100": "Mass or Volume Air Flow Circuit", - "P0101": "Mass or Volume Air Flow Circuit Range/Performance", - "P0102": "Mass or Volume Air Flow Circuit Low Input", - "P0103": "Mass or Volume Air Flow Circuit High Input", - "P0104": "Mass or Volume Air Flow Circuit Intermittent", - "P0105": "Manifold Absolute Pressure/Barometric Pressure Circuit", - "P0106": "Manifold Absolute Pressure/Barometric Pressure Circuit Range/Performance", - "P0107": "Manifold Absolute Pressure/Barometric Pressure Circuit Low Input", - "P0108": "Manifold Absolute Pressure/Barometric Pressure Circuit High Input", - "P0109": "Manifold Absolute Pressure/Barometric Pressure Circuit Intermittent", - "P0110": "Intake Air Temperature Sensor 1 Circuit", - "P0111": "Intake Air Temperature Sensor 1 Circuit Range/Performance", - "P0112": "Intake Air Temperature Sensor 1 Circuit Low", - "P0113": "Intake Air Temperature Sensor 1 Circuit High", - "P0114": "Intake Air Temperature Sensor 1 Circuit Intermittent", - "P0115": "Engine Coolant Temperature Circuit", - "P0116": "Engine Coolant Temperature Circuit Range/Performance", - "P0117": "Engine Coolant Temperature Circuit Low", - "P0118": "Engine Coolant Temperature Circuit High", - "P0119": "Engine Coolant Temperature Circuit Intermittent", - "P0120": "Throttle/Pedal Position Sensor/Switch 'A' Circuit", - "P0121": "Throttle/Pedal Position Sensor/Switch 'A' Circuit Range/Performance", - "P0122": "Throttle/Pedal Position Sensor/Switch 'A' Circuit Low", - "P0123": "Throttle/Pedal Position Sensor/Switch 'A' Circuit High", - "P0124": "Throttle/Pedal Position Sensor/Switch 'A' Circuit Intermittent", - "P0125": "Insufficient Coolant Temperature for Closed Loop Fuel Control", - "P0126": "Insufficient Coolant Temperature for Stable Operation", - "P0127": "Intake Air Temperature Too High", - "P0128": "Coolant Thermostat (Coolant Temperature Below Thermostat Regulating Temperature)", - "P0129": "Barometric Pressure Too Low", - "P0130": "O2 Sensor Circuit", - "P0131": "O2 Sensor Circuit Low Voltage", - "P0132": "O2 Sensor Circuit High Voltage", - "P0133": "O2 Sensor Circuit Slow Response", - "P0134": "O2 Sensor Circuit No Activity Detected", - "P0135": "O2 Sensor Heater Circuit", - "P0136": "O2 Sensor Circuit", - "P0137": "O2 Sensor Circuit Low Voltage", - "P0138": "O2 Sensor Circuit High Voltage", - "P0139": "O2 Sensor Circuit Slow Response", - "P0140": "O2 Sensor Circuit No Activity Detected", - "P0141": "O2 Sensor Heater Circuit", - "P0142": "O2 Sensor Circuit", - "P0143": "O2 Sensor Circuit Low Voltage", - "P0144": "O2 Sensor Circuit High Voltage", - "P0145": "O2 Sensor Circuit Slow Response", - "P0146": "O2 Sensor Circuit No Activity Detected", - "P0147": "O2 Sensor Heater Circuit", - "P0148": "Fuel Delivery Error", - "P0149": "Fuel Timing Error", - "P0150": "O2 Sensor Circuit", - "P0151": "O2 Sensor Circuit Low Voltage", - "P0152": "O2 Sensor Circuit High Voltage", - "P0153": "O2 Sensor Circuit Slow Response", - "P0154": "O2 Sensor Circuit No Activity Detected", - "P0155": "O2 Sensor Heater Circuit", - "P0156": "O2 Sensor Circuit", - "P0157": "O2 Sensor Circuit Low Voltage", - "P0158": "O2 Sensor Circuit High Voltage", - "P0159": "O2 Sensor Circuit Slow Response", - "P0160": "O2 Sensor Circuit No Activity Detected", - "P0161": "O2 Sensor Heater Circuit", - "P0162": "O2 Sensor Circuit", - "P0163": "O2 Sensor Circuit Low Voltage", - "P0164": "O2 Sensor Circuit High Voltage", - "P0165": "O2 Sensor Circuit Slow Response", - "P0166": "O2 Sensor Circuit No Activity Detected", - "P0167": "O2 Sensor Heater Circuit", - "P0168": "Fuel Temperature Too High", - "P0169": "Incorrect Fuel Composition", - "P0170": "Fuel Trim", - "P0171": "System Too Lean", - "P0172": "System Too Rich", - "P0173": "Fuel Trim", - "P0174": "System Too Lean", - "P0175": "System Too Rich", - "P0176": "Fuel Composition Sensor Circuit", - "P0177": "Fuel Composition Sensor Circuit Range/Performance", - "P0178": "Fuel Composition Sensor Circuit Low", - "P0179": "Fuel Composition Sensor Circuit High", - "P0180": "Fuel Temperature Sensor A Circuit", - "P0181": "Fuel Temperature Sensor A Circuit Range/Performance", - "P0182": "Fuel Temperature Sensor A Circuit Low", - "P0183": "Fuel Temperature Sensor A Circuit High", - "P0184": "Fuel Temperature Sensor A Circuit Intermittent", - "P0185": "Fuel Temperature Sensor B Circuit", - "P0186": "Fuel Temperature Sensor B Circuit Range/Performance", - "P0187": "Fuel Temperature Sensor B Circuit Low", - "P0188": "Fuel Temperature Sensor B Circuit High", - "P0189": "Fuel Temperature Sensor B Circuit Intermittent", - "P0190": "Fuel Rail Pressure Sensor Circuit", - "P0191": "Fuel Rail Pressure Sensor Circuit Range/Performance", - "P0192": "Fuel Rail Pressure Sensor Circuit Low", - "P0193": "Fuel Rail Pressure Sensor Circuit High", - "P0194": "Fuel Rail Pressure Sensor Circuit Intermittent", - "P0195": "Engine Oil Temperature Sensor", - "P0196": "Engine Oil Temperature Sensor Range/Performance", - "P0197": "Engine Oil Temperature Sensor Low", - "P0198": "Engine Oil Temperature Sensor High", - "P0199": "Engine Oil Temperature Sensor Intermittent", - "P0200": "Injector Circuit/Open", - "P0201": "Injector Circuit/Open - Cylinder 1", - "P0202": "Injector Circuit/Open - Cylinder 2", - "P0203": "Injector Circuit/Open - Cylinder 3", - "P0204": "Injector Circuit/Open - Cylinder 4", - "P0205": "Injector Circuit/Open - Cylinder 5", - "P0206": "Injector Circuit/Open - Cylinder 6", - "P0207": "Injector Circuit/Open - Cylinder 7", - "P0208": "Injector Circuit/Open - Cylinder 8", - "P0209": "Injector Circuit/Open - Cylinder 9", - "P0210": "Injector Circuit/Open - Cylinder 10", - "P0211": "Injector Circuit/Open - Cylinder 11", - "P0212": "Injector Circuit/Open - Cylinder 12", - "P0213": "Cold Start Injector 1", - "P0214": "Cold Start Injector 2", - "P0215": "Engine Shutoff Solenoid", - "P0216": "Injector/Injection Timing Control Circuit", - "P0217": "Engine Coolant Over Temperature Condition", - "P0218": "Transmission Fluid Over Temperature Condition", - "P0219": "Engine Overspeed Condition", - "P0220": "Throttle/Pedal Position Sensor/Switch 'B' Circuit", - "P0221": "Throttle/Pedal Position Sensor/Switch 'B' Circuit Range/Performance", - "P0222": "Throttle/Pedal Position Sensor/Switch 'B' Circuit Low", - "P0223": "Throttle/Pedal Position Sensor/Switch 'B' Circuit High", - "P0224": "Throttle/Pedal Position Sensor/Switch 'B' Circuit Intermittent", - "P0225": "Throttle/Pedal Position Sensor/Switch 'C' Circuit", - "P0226": "Throttle/Pedal Position Sensor/Switch 'C' Circuit Range/Performance", - "P0227": "Throttle/Pedal Position Sensor/Switch 'C' Circuit Low", - "P0228": "Throttle/Pedal Position Sensor/Switch 'C' Circuit High", - "P0229": "Throttle/Pedal Position Sensor/Switch 'C' Circuit Intermittent", - "P0230": "Fuel Pump Primary Circuit", - "P0231": "Fuel Pump Secondary Circuit Low", - "P0232": "Fuel Pump Secondary Circuit High", - "P0233": "Fuel Pump Secondary Circuit Intermittent", - "P0234": "Turbo/Super Charger Overboost Condition", - "P0235": "Turbo/Super Charger Boost Sensor 'A' Circuit", - "P0236": "Turbo/Super Charger Boost Sensor 'A' Circuit Range/Performance", - "P0237": "Turbo/Super Charger Boost Sensor 'A' Circuit Low", - "P0238": "Turbo/Super Charger Boost Sensor 'A' Circuit High", - "P0239": "Turbo/Super Charger Boost Sensor 'B' Circuit", - "P0240": "Turbo/Super Charger Boost Sensor 'B' Circuit Range/Performance", - "P0241": "Turbo/Super Charger Boost Sensor 'B' Circuit Low", - "P0242": "Turbo/Super Charger Boost Sensor 'B' Circuit High", - "P0243": "Turbo/Super Charger Wastegate Solenoid 'A'", - "P0244": "Turbo/Super Charger Wastegate Solenoid 'A' Range/Performance", - "P0245": "Turbo/Super Charger Wastegate Solenoid 'A' Low", - "P0246": "Turbo/Super Charger Wastegate Solenoid 'A' High", - "P0247": "Turbo/Super Charger Wastegate Solenoid 'B'", - "P0248": "Turbo/Super Charger Wastegate Solenoid 'B' Range/Performance", - "P0249": "Turbo/Super Charger Wastegate Solenoid 'B' Low", - "P0250": "Turbo/Super Charger Wastegate Solenoid 'B' High", - "P0251": "Injection Pump Fuel Metering Control 'A' (Cam/Rotor/Injector)", - "P0252": "Injection Pump Fuel Metering Control 'A' Range/Performance (Cam/Rotor/Injector)", - "P0253": "Injection Pump Fuel Metering Control 'A' Low (Cam/Rotor/Injector)", - "P0254": "Injection Pump Fuel Metering Control 'A' High (Cam/Rotor/Injector)", - "P0255": "Injection Pump Fuel Metering Control 'A' Intermittent (Cam/Rotor/Injector)", - "P0256": "Injection Pump Fuel Metering Control 'B' (Cam/Rotor/Injector)", - "P0257": "Injection Pump Fuel Metering Control 'B' Range/Performance (Cam/Rotor/Injector)", - "P0258": "Injection Pump Fuel Metering Control 'B' Low (Cam/Rotor/Injector)", - "P0259": "Injection Pump Fuel Metering Control 'B' High (Cam/Rotor/Injector)", - "P0260": "Injection Pump Fuel Metering Control 'B' Intermittent (Cam/Rotor/Injector)", - "P0261": "Cylinder 1 Injector Circuit Low", - "P0262": "Cylinder 1 Injector Circuit High", - "P0263": "Cylinder 1 Contribution/Balance", - "P0264": "Cylinder 2 Injector Circuit Low", - "P0265": "Cylinder 2 Injector Circuit High", - "P0266": "Cylinder 2 Contribution/Balance", - "P0267": "Cylinder 3 Injector Circuit Low", - "P0268": "Cylinder 3 Injector Circuit High", - "P0269": "Cylinder 3 Contribution/Balance", - "P0270": "Cylinder 4 Injector Circuit Low", - "P0271": "Cylinder 4 Injector Circuit High", - "P0272": "Cylinder 4 Contribution/Balance", - "P0273": "Cylinder 5 Injector Circuit Low", - "P0274": "Cylinder 5 Injector Circuit High", - "P0275": "Cylinder 5 Contribution/Balance", - "P0276": "Cylinder 6 Injector Circuit Low", - "P0277": "Cylinder 6 Injector Circuit High", - "P0278": "Cylinder 6 Contribution/Balance", - "P0279": "Cylinder 7 Injector Circuit Low", - "P0280": "Cylinder 7 Injector Circuit High", - "P0281": "Cylinder 7 Contribution/Balance", - "P0282": "Cylinder 8 Injector Circuit Low", - "P0283": "Cylinder 8 Injector Circuit High", - "P0284": "Cylinder 8 Contribution/Balance", - "P0285": "Cylinder 9 Injector Circuit Low", - "P0286": "Cylinder 9 Injector Circuit High", - "P0287": "Cylinder 9 Contribution/Balance", - "P0288": "Cylinder 10 Injector Circuit Low", - "P0289": "Cylinder 10 Injector Circuit High", - "P0290": "Cylinder 10 Contribution/Balance", - "P0291": "Cylinder 11 Injector Circuit Low", - "P0292": "Cylinder 11 Injector Circuit High", - "P0293": "Cylinder 11 Contribution/Balance", - "P0294": "Cylinder 12 Injector Circuit Low", - "P0295": "Cylinder 12 Injector Circuit High", - "P0296": "Cylinder 12 Contribution/Balance", - "P0297": "Vehicle Overspeed Condition", - "P0298": "Engine Oil Over Temperature", - "P0299": "Turbo/Super Charger Underboost", - "P0300": "Random/Multiple Cylinder Misfire Detected", - "P0301": "Cylinder 1 Misfire Detected", - "P0302": "Cylinder 2 Misfire Detected", - "P0303": "Cylinder 3 Misfire Detected", - "P0304": "Cylinder 4 Misfire Detected", - "P0305": "Cylinder 5 Misfire Detected", - "P0306": "Cylinder 6 Misfire Detected", - "P0307": "Cylinder 7 Misfire Detected", - "P0308": "Cylinder 8 Misfire Detected", - "P0309": "Cylinder 9 Misfire Detected", - "P0310": "Cylinder 10 Misfire Detected", - "P0311": "Cylinder 11 Misfire Detected", - "P0312": "Cylinder 12 Misfire Detected", - "P0313": "Misfire Detected with Low Fuel", - "P0314": "Single Cylinder Misfire (Cylinder not Specified)", - "P0315": "Crankshaft Position System Variation Not Learned", - "P0316": "Engine Misfire Detected on Startup (First 1000 Revolutions)", - "P0317": "Rough Road Hardware Not Present", - "P0318": "Rough Road Sensor 'A' Signal Circuit", - "P0319": "Rough Road Sensor 'B'", - "P0320": "Ignition/Distributor Engine Speed Input Circuit", - "P0321": "Ignition/Distributor Engine Speed Input Circuit Range/Performance", - "P0322": "Ignition/Distributor Engine Speed Input Circuit No Signal", - "P0323": "Ignition/Distributor Engine Speed Input Circuit Intermittent", - "P0324": "Knock Control System Error", - "P0325": "Knock Sensor 1 Circuit", - "P0326": "Knock Sensor 1 Circuit Range/Performance", - "P0327": "Knock Sensor 1 Circuit Low", - "P0328": "Knock Sensor 1 Circuit High", - "P0329": "Knock Sensor 1 Circuit Input Intermittent", - "P0330": "Knock Sensor 2 Circuit", - "P0331": "Knock Sensor 2 Circuit Range/Performance", - "P0332": "Knock Sensor 2 Circuit Low", - "P0333": "Knock Sensor 2 Circuit High", - "P0334": "Knock Sensor 2 Circuit Input Intermittent", - "P0335": "Crankshaft Position Sensor 'A' Circuit", - "P0336": "Crankshaft Position Sensor 'A' Circuit Range/Performance", - "P0337": "Crankshaft Position Sensor 'A' Circuit Low", - "P0338": "Crankshaft Position Sensor 'A' Circuit High", - "P0339": "Crankshaft Position Sensor 'A' Circuit Intermittent", - "P0340": "Camshaft Position Sensor 'A' Circuit", - "P0341": "Camshaft Position Sensor 'A' Circuit Range/Performance", - "P0342": "Camshaft Position Sensor 'A' Circuit Low", - "P0343": "Camshaft Position Sensor 'A' Circuit High", - "P0344": "Camshaft Position Sensor 'A' Circuit Intermittent", - "P0345": "Camshaft Position Sensor 'A' Circuit", - "P0346": "Camshaft Position Sensor 'A' Circuit Range/Performance", - "P0347": "Camshaft Position Sensor 'A' Circuit Low", - "P0348": "Camshaft Position Sensor 'A' Circuit High", - "P0349": "Camshaft Position Sensor 'A' Circuit Intermittent", - "P0350": "Ignition Coil Primary/Secondary Circuit", - "P0351": "Ignition Coil 'A' Primary/Secondary Circuit", - "P0352": "Ignition Coil 'B' Primary/Secondary Circuit", - "P0353": "Ignition Coil 'C' Primary/Secondary Circuit", - "P0354": "Ignition Coil 'D' Primary/Secondary Circuit", - "P0355": "Ignition Coil 'E' Primary/Secondary Circuit", - "P0356": "Ignition Coil 'F' Primary/Secondary Circuit", - "P0357": "Ignition Coil 'G' Primary/Secondary Circuit", - "P0358": "Ignition Coil 'H' Primary/Secondary Circuit", - "P0359": "Ignition Coil 'I' Primary/Secondary Circuit", - "P0360": "Ignition Coil 'J' Primary/Secondary Circuit", - "P0361": "Ignition Coil 'K' Primary/Secondary Circuit", - "P0362": "Ignition Coil 'L' Primary/Secondary Circuit", - "P0363": "Misfire Detected - Fueling Disabled", - "P0364": "Reserved", - "P0365": "Camshaft Position Sensor 'B' Circuit", - "P0366": "Camshaft Position Sensor 'B' Circuit Range/Performance", - "P0367": "Camshaft Position Sensor 'B' Circuit Low", - "P0368": "Camshaft Position Sensor 'B' Circuit High", - "P0369": "Camshaft Position Sensor 'B' Circuit Intermittent", - "P0370": "Timing Reference High Resolution Signal 'A'", - "P0371": "Timing Reference High Resolution Signal 'A' Too Many Pulses", - "P0372": "Timing Reference High Resolution Signal 'A' Too Few Pulses", - "P0373": "Timing Reference High Resolution Signal 'A' Intermittent/Erratic Pulses", - "P0374": "Timing Reference High Resolution Signal 'A' No Pulse", - "P0375": "Timing Reference High Resolution Signal 'B'", - "P0376": "Timing Reference High Resolution Signal 'B' Too Many Pulses", - "P0377": "Timing Reference High Resolution Signal 'B' Too Few Pulses", - "P0378": "Timing Reference High Resolution Signal 'B' Intermittent/Erratic Pulses", - "P0379": "Timing Reference High Resolution Signal 'B' No Pulses", - "P0380": "Glow Plug/Heater Circuit 'A'", - "P0381": "Glow Plug/Heater Indicator Circuit", - "P0382": "Glow Plug/Heater Circuit 'B'", - "P0383": "Reserved by SAE J2012", - "P0384": "Reserved by SAE J2012", - "P0385": "Crankshaft Position Sensor 'B' Circuit", - "P0386": "Crankshaft Position Sensor 'B' Circuit Range/Performance", - "P0387": "Crankshaft Position Sensor 'B' Circuit Low", - "P0388": "Crankshaft Position Sensor 'B' Circuit High", - "P0389": "Crankshaft Position Sensor 'B' Circuit Intermittent", - "P0390": "Camshaft Position Sensor 'B' Circuit", - "P0391": "Camshaft Position Sensor 'B' Circuit Range/Performance", - "P0392": "Camshaft Position Sensor 'B' Circuit Low", - "P0393": "Camshaft Position Sensor 'B' Circuit High", - "P0394": "Camshaft Position Sensor 'B' Circuit Intermittent", - "P0400": "Exhaust Gas Recirculation Flow", - "P0401": "Exhaust Gas Recirculation Flow Insufficient Detected", - "P0402": "Exhaust Gas Recirculation Flow Excessive Detected", - "P0403": "Exhaust Gas Recirculation Control Circuit", - "P0404": "Exhaust Gas Recirculation Control Circuit Range/Performance", - "P0405": "Exhaust Gas Recirculation Sensor 'A' Circuit Low", - "P0406": "Exhaust Gas Recirculation Sensor 'A' Circuit High", - "P0407": "Exhaust Gas Recirculation Sensor 'B' Circuit Low", - "P0408": "Exhaust Gas Recirculation Sensor 'B' Circuit High", - "P0409": "Exhaust Gas Recirculation Sensor 'A' Circuit", - "P0410": "Secondary Air Injection System", - "P0411": "Secondary Air Injection System Incorrect Flow Detected", - "P0412": "Secondary Air Injection System Switching Valve 'A' Circuit", - "P0413": "Secondary Air Injection System Switching Valve 'A' Circuit Open", - "P0414": "Secondary Air Injection System Switching Valve 'A' Circuit Shorted", - "P0415": "Secondary Air Injection System Switching Valve 'B' Circuit", - "P0416": "Secondary Air Injection System Switching Valve 'B' Circuit Open", - "P0417": "Secondary Air Injection System Switching Valve 'B' Circuit Shorted", - "P0418": "Secondary Air Injection System Control 'A' Circuit", - "P0419": "Secondary Air Injection System Control 'B' Circuit", - "P0420": "Catalyst System Efficiency Below Threshold", - "P0421": "Warm Up Catalyst Efficiency Below Threshold", - "P0422": "Main Catalyst Efficiency Below Threshold", - "P0423": "Heated Catalyst Efficiency Below Threshold", - "P0424": "Heated Catalyst Temperature Below Threshold", - "P0425": "Catalyst Temperature Sensor", - "P0426": "Catalyst Temperature Sensor Range/Performance", - "P0427": "Catalyst Temperature Sensor Low", - "P0428": "Catalyst Temperature Sensor High", - "P0429": "Catalyst Heater Control Circuit", - "P0430": "Catalyst System Efficiency Below Threshold", - "P0431": "Warm Up Catalyst Efficiency Below Threshold", - "P0432": "Main Catalyst Efficiency Below Threshold", - "P0433": "Heated Catalyst Efficiency Below Threshold", - "P0434": "Heated Catalyst Temperature Below Threshold", - "P0435": "Catalyst Temperature Sensor", - "P0436": "Catalyst Temperature Sensor Range/Performance", - "P0437": "Catalyst Temperature Sensor Low", - "P0438": "Catalyst Temperature Sensor High", - "P0439": "Catalyst Heater Control Circuit", - "P0440": "Evaporative Emission System", - "P0441": "Evaporative Emission System Incorrect Purge Flow", - "P0442": "Evaporative Emission System Leak Detected (small leak)", - "P0443": "Evaporative Emission System Purge Control Valve Circuit", - "P0444": "Evaporative Emission System Purge Control Valve Circuit Open", - "P0445": "Evaporative Emission System Purge Control Valve Circuit Shorted", - "P0446": "Evaporative Emission System Vent Control Circuit", - "P0447": "Evaporative Emission System Vent Control Circuit Open", - "P0448": "Evaporative Emission System Vent Control Circuit Shorted", - "P0449": "Evaporative Emission System Vent Valve/Solenoid Circuit", - "P0450": "Evaporative Emission System Pressure Sensor/Switch", - "P0451": "Evaporative Emission System Pressure Sensor/Switch Range/Performance", - "P0452": "Evaporative Emission System Pressure Sensor/Switch Low", - "P0453": "Evaporative Emission System Pressure Sensor/Switch High", - "P0454": "Evaporative Emission System Pressure Sensor/Switch Intermittent", - "P0455": "Evaporative Emission System Leak Detected (large leak)", - "P0456": "Evaporative Emission System Leak Detected (very small leak)", - "P0457": "Evaporative Emission System Leak Detected (fuel cap loose/off)", - "P0458": "Evaporative Emission System Purge Control Valve Circuit Low", - "P0459": "Evaporative Emission System Purge Control Valve Circuit High", - "P0460": "Fuel Level Sensor 'A' Circuit", - "P0461": "Fuel Level Sensor 'A' Circuit Range/Performance", - "P0462": "Fuel Level Sensor 'A' Circuit Low", - "P0463": "Fuel Level Sensor 'A' Circuit High", - "P0464": "Fuel Level Sensor 'A' Circuit Intermittent", - "P0465": "EVAP Purge Flow Sensor Circuit", - "P0466": "EVAP Purge Flow Sensor Circuit Range/Performance", - "P0467": "EVAP Purge Flow Sensor Circuit Low", - "P0468": "EVAP Purge Flow Sensor Circuit High", - "P0469": "EVAP Purge Flow Sensor Circuit Intermittent", - "P0470": "Exhaust Pressure Sensor", - "P0471": "Exhaust Pressure Sensor Range/Performance", - "P0472": "Exhaust Pressure Sensor Low", - "P0473": "Exhaust Pressure Sensor High", - "P0474": "Exhaust Pressure Sensor Intermittent", - "P0475": "Exhaust Pressure Control Valve", - "P0476": "Exhaust Pressure Control Valve Range/Performance", - "P0477": "Exhaust Pressure Control Valve Low", - "P0478": "Exhaust Pressure Control Valve High", - "P0479": "Exhaust Pressure Control Valve Intermittent", - "P0480": "Fan 1 Control Circuit", - "P0481": "Fan 2 Control Circuit", - "P0482": "Fan 3 Control Circuit", - "P0483": "Fan Rationality Check", - "P0484": "Fan Circuit Over Current", - "P0485": "Fan Power/Ground Circuit", - "P0486": "Exhaust Gas Recirculation Sensor 'B' Circuit", - "P0487": "Exhaust Gas Recirculation Throttle Position Control Circuit", - "P0488": "Exhaust Gas Recirculation Throttle Position Control Range/Performance", - "P0489": "Exhaust Gas Recirculation Control Circuit Low", - "P0490": "Exhaust Gas Recirculation Control Circuit High", - "P0491": "Secondary Air Injection System Insufficient Flow", - "P0492": "Secondary Air Injection System Insufficient Flow", - "P0493": "Fan Overspeed", - "P0494": "Fan Speed Low", - "P0495": "Fan Speed High", - "P0496": "Evaporative Emission System High Purge Flow", - "P0497": "Evaporative Emission System Low Purge Flow", - "P0498": "Evaporative Emission System Vent Valve Control Circuit Low", - "P0499": "Evaporative Emission System Vent Valve Control Circuit High", - "P0500": "Vehicle Speed Sensor 'A'", - "P0501": "Vehicle Speed Sensor 'A' Range/Performance", - "P0502": "Vehicle Speed Sensor 'A' Circuit Low Input", - "P0503": "Vehicle Speed Sensor 'A' Intermittent/Erratic/High", - "P0504": "Brake Switch 'A'/'B' Correlation", - "P0505": "Idle Air Control System", - "P0506": "Idle Air Control System RPM Lower Than Expected", - "P0507": "Idle Air Control System RPM Higher Than Expected", - "P0508": "Idle Air Control System Circuit Low", - "P0509": "Idle Air Control System Circuit High", - "P0510": "Closed Throttle Position Switch", - "P0511": "Idle Air Control Circuit", - "P0512": "Starter Request Circuit", - "P0513": "Incorrect Immobilizer Key", - "P0514": "Battery Temperature Sensor Circuit Range/Performance", - "P0515": "Battery Temperature Sensor Circuit", - "P0516": "Battery Temperature Sensor Circuit Low", - "P0517": "Battery Temperature Sensor Circuit High", - "P0518": "Idle Air Control Circuit Intermittent", - "P0519": "Idle Air Control System Performance", - "P0520": "Engine Oil Pressure Sensor/Switch Circuit", - "P0521": "Engine Oil Pressure Sensor/Switch Range/Performance", - "P0522": "Engine Oil Pressure Sensor/Switch Low Voltage", - "P0523": "Engine Oil Pressure Sensor/Switch High Voltage", - "P0524": "Engine Oil Pressure Too Low", - "P0525": "Cruise Control Servo Control Circuit Range/Performance", - "P0526": "Fan Speed Sensor Circuit", - "P0527": "Fan Speed Sensor Circuit Range/Performance", - "P0528": "Fan Speed Sensor Circuit No Signal", - "P0529": "Fan Speed Sensor Circuit Intermittent", - "P0530": "A/C Refrigerant Pressure Sensor 'A' Circuit", - "P0531": "A/C Refrigerant Pressure Sensor 'A' Circuit Range/Performance", - "P0532": "A/C Refrigerant Pressure Sensor 'A' Circuit Low", - "P0533": "A/C Refrigerant Pressure Sensor 'A' Circuit High", - "P0534": "Air Conditioner Refrigerant Charge Loss", - "P0535": "A/C Evaporator Temperature Sensor Circuit", - "P0536": "A/C Evaporator Temperature Sensor Circuit Range/Performance", - "P0537": "A/C Evaporator Temperature Sensor Circuit Low", - "P0538": "A/C Evaporator Temperature Sensor Circuit High", - "P0539": "A/C Evaporator Temperature Sensor Circuit Intermittent", - "P0540": "Intake Air Heater 'A' Circuit", - "P0541": "Intake Air Heater 'A' Circuit Low", - "P0542": "Intake Air Heater 'A' Circuit High", - "P0543": "Intake Air Heater 'A' Circuit Open", - "P0544": "Exhaust Gas Temperature Sensor Circuit", - "P0545": "Exhaust Gas Temperature Sensor Circuit Low", - "P0546": "Exhaust Gas Temperature Sensor Circuit High", - "P0547": "Exhaust Gas Temperature Sensor Circuit", - "P0548": "Exhaust Gas Temperature Sensor Circuit Low", - "P0549": "Exhaust Gas Temperature Sensor Circuit High", - "P0550": "Power Steering Pressure Sensor/Switch Circuit", - "P0551": "Power Steering Pressure Sensor/Switch Circuit Range/Performance", - "P0552": "Power Steering Pressure Sensor/Switch Circuit Low Input", - "P0553": "Power Steering Pressure Sensor/Switch Circuit High Input", - "P0554": "Power Steering Pressure Sensor/Switch Circuit Intermittent", - "P0555": "Brake Booster Pressure Sensor Circuit", - "P0556": "Brake Booster Pressure Sensor Circuit Range/Performance", - "P0557": "Brake Booster Pressure Sensor Circuit Low Input", - "P0558": "Brake Booster Pressure Sensor Circuit High Input", - "P0559": "Brake Booster Pressure Sensor Circuit Intermittent", - "P0560": "System Voltage", - "P0561": "System Voltage Unstable", - "P0562": "System Voltage Low", - "P0563": "System Voltage High", - "P0564": "Cruise Control Multi-Function Input 'A' Circuit", - "P0565": "Cruise Control On Signal", - "P0566": "Cruise Control Off Signal", - "P0567": "Cruise Control Resume Signal", - "P0568": "Cruise Control Set Signal", - "P0569": "Cruise Control Coast Signal", - "P0570": "Cruise Control Accelerate Signal", - "P0571": "Brake Switch 'A' Circuit", - "P0572": "Brake Switch 'A' Circuit Low", - "P0573": "Brake Switch 'A' Circuit High", - "P0574": "Cruise Control System - Vehicle Speed Too High", - "P0575": "Cruise Control Input Circuit", - "P0576": "Cruise Control Input Circuit Low", - "P0577": "Cruise Control Input Circuit High", - "P0578": "Cruise Control Multi-Function Input 'A' Circuit Stuck", - "P0579": "Cruise Control Multi-Function Input 'A' Circuit Range/Performance", - "P0580": "Cruise Control Multi-Function Input 'A' Circuit Low", - "P0581": "Cruise Control Multi-Function Input 'A' Circuit High", - "P0582": "Cruise Control Vacuum Control Circuit/Open", - "P0583": "Cruise Control Vacuum Control Circuit Low", - "P0584": "Cruise Control Vacuum Control Circuit High", - "P0585": "Cruise Control Multi-Function Input 'A'/'B' Correlation", - "P0586": "Cruise Control Vent Control Circuit/Open", - "P0587": "Cruise Control Vent Control Circuit Low", - "P0588": "Cruise Control Vent Control Circuit High", - "P0589": "Cruise Control Multi-Function Input 'B' Circuit", - "P0590": "Cruise Control Multi-Function Input 'B' Circuit Stuck", - "P0591": "Cruise Control Multi-Function Input 'B' Circuit Range/Performance", - "P0592": "Cruise Control Multi-Function Input 'B' Circuit Low", - "P0593": "Cruise Control Multi-Function Input 'B' Circuit High", - "P0594": "Cruise Control Servo Control Circuit/Open", - "P0595": "Cruise Control Servo Control Circuit Low", - "P0596": "Cruise Control Servo Control Circuit High", - "P0597": "Thermostat Heater Control Circuit/Open", - "P0598": "Thermostat Heater Control Circuit Low", - "P0599": "Thermostat Heater Control Circuit High", - "P0600": "Serial Communication Link", - "P0601": "Internal Control Module Memory Check Sum Error", - "P0602": "Control Module Programming Error", - "P0603": "Internal Control Module Keep Alive Memory (KAM) Error", - "P0604": "Internal Control Module Random Access Memory (RAM) Error", - "P0605": "Internal Control Module Read Only Memory (ROM) Error", - "P0606": "ECM/PCM Processor", - "P0607": "Control Module Performance", - "P0608": "Control Module VSS Output 'A'", - "P0609": "Control Module VSS Output 'B'", - "P0610": "Control Module Vehicle Options Error", - "P0611": "Fuel Injector Control Module Performance", - "P0612": "Fuel Injector Control Module Relay Control", - "P0613": "TCM Processor", - "P0614": "ECM / TCM Incompatible", - "P0615": "Starter Relay Circuit", - "P0616": "Starter Relay Circuit Low", - "P0617": "Starter Relay Circuit High", - "P0618": "Alternative Fuel Control Module KAM Error", - "P0619": "Alternative Fuel Control Module RAM/ROM Error", - "P0620": "Generator Control Circuit", - "P0621": "Generator Lamp/L Terminal Circuit", - "P0622": "Generator Field/F Terminal Circuit", - "P0623": "Generator Lamp Control Circuit", - "P0624": "Fuel Cap Lamp Control Circuit", - "P0625": "Generator Field/F Terminal Circuit Low", - "P0626": "Generator Field/F Terminal Circuit High", - "P0627": "Fuel Pump 'A' Control Circuit /Open", - "P0628": "Fuel Pump 'A' Control Circuit Low", - "P0629": "Fuel Pump 'A' Control Circuit High", - "P0630": "VIN Not Programmed or Incompatible - ECM/PCM", - "P0631": "VIN Not Programmed or Incompatible - TCM", - "P0632": "Odometer Not Programmed - ECM/PCM", - "P0633": "Immobilizer Key Not Programmed - ECM/PCM", - "P0634": "PCM/ECM/TCM Internal Temperature Too High", - "P0635": "Power Steering Control Circuit", - "P0636": "Power Steering Control Circuit Low", - "P0637": "Power Steering Control Circuit High", - "P0638": "Throttle Actuator Control Range/Performance", - "P0639": "Throttle Actuator Control Range/Performance", - "P0640": "Intake Air Heater Control Circuit", - "P0641": "Sensor Reference Voltage 'A' Circuit/Open", - "P0642": "Sensor Reference Voltage 'A' Circuit Low", - "P0643": "Sensor Reference Voltage 'A' Circuit High", - "P0644": "Driver Display Serial Communication Circuit", - "P0645": "A/C Clutch Relay Control Circuit", - "P0646": "A/C Clutch Relay Control Circuit Low", - "P0647": "A/C Clutch Relay Control Circuit High", - "P0648": "Immobilizer Lamp Control Circuit", - "P0649": "Speed Control Lamp Control Circuit", - "P0650": "Malfunction Indicator Lamp (MIL) Control Circuit", - "P0651": "Sensor Reference Voltage 'B' Circuit/Open", - "P0652": "Sensor Reference Voltage 'B' Circuit Low", - "P0653": "Sensor Reference Voltage 'B' Circuit High", - "P0654": "Engine RPM Output Circuit", - "P0655": "Engine Hot Lamp Output Control Circuit", - "P0656": "Fuel Level Output Circuit", - "P0657": "Actuator Supply Voltage 'A' Circuit/Open", - "P0658": "Actuator Supply Voltage 'A' Circuit Low", - "P0659": "Actuator Supply Voltage 'A' Circuit High", - "P0660": "Intake Manifold Tuning Valve Control Circuit/Open", - "P0661": "Intake Manifold Tuning Valve Control Circuit Low", - "P0662": "Intake Manifold Tuning Valve Control Circuit High", - "P0663": "Intake Manifold Tuning Valve Control Circuit/Open", - "P0664": "Intake Manifold Tuning Valve Control Circuit Low", - "P0665": "Intake Manifold Tuning Valve Control Circuit High", - "P0666": "PCM/ECM/TCM Internal Temperature Sensor Circuit", - "P0667": "PCM/ECM/TCM Internal Temperature Sensor Range/Performance", - "P0668": "PCM/ECM/TCM Internal Temperature Sensor Circuit Low", - "P0669": "PCM/ECM/TCM Internal Temperature Sensor Circuit High", - "P0670": "Glow Plug Module Control Circuit", - "P0671": "Cylinder 1 Glow Plug Circuit", - "P0672": "Cylinder 2 Glow Plug Circuit", - "P0673": "Cylinder 3 Glow Plug Circuit", - "P0674": "Cylinder 4 Glow Plug Circuit", - "P0675": "Cylinder 5 Glow Plug Circuit", - "P0676": "Cylinder 6 Glow Plug Circuit", - "P0677": "Cylinder 7 Glow Plug Circuit", - "P0678": "Cylinder 8 Glow Plug Circuit", - "P0679": "Cylinder 9 Glow Plug Circuit", - "P0680": "Cylinder 10 Glow Plug Circuit", - "P0681": "Cylinder 11 Glow Plug Circuit", - "P0682": "Cylinder 12 Glow Plug Circuit", - "P0683": "Glow Plug Control Module to PCM Communication Circuit", - "P0684": "Glow Plug Control Module to PCM Communication Circuit Range/Performance", - "P0685": "ECM/PCM Power Relay Control Circuit /Open", - "P0686": "ECM/PCM Power Relay Control Circuit Low", - "P0687": "ECM/PCM Power Relay Control Circuit High", - "P0688": "ECM/PCM Power Relay Sense Circuit /Open", - "P0689": "ECM/PCM Power Relay Sense Circuit Low", - "P0690": "ECM/PCM Power Relay Sense Circuit High", - "P0691": "Fan 1 Control Circuit Low", - "P0692": "Fan 1 Control Circuit High", - "P0693": "Fan 2 Control Circuit Low", - "P0694": "Fan 2 Control Circuit High", - "P0695": "Fan 3 Control Circuit Low", - "P0696": "Fan 3 Control Circuit High", - "P0697": "Sensor Reference Voltage 'C' Circuit/Open", - "P0698": "Sensor Reference Voltage 'C' Circuit Low", - "P0699": "Sensor Reference Voltage 'C' Circuit High", - "P0700": "Transmission Control System (MIL Request)", - "P0701": "Transmission Control System Range/Performance", - "P0702": "Transmission Control System Electrical", - "P0703": "Brake Switch 'B' Circuit", - "P0704": "Clutch Switch Input Circuit Malfunction", - "P0705": "Transmission Range Sensor Circuit Malfunction (PRNDL Input)", - "P0706": "Transmission Range Sensor Circuit Range/Performance", - "P0707": "Transmission Range Sensor Circuit Low", - "P0708": "Transmission Range Sensor Circuit High", - "P0709": "Transmission Range Sensor Circuit Intermittent", - "P0710": "Transmission Fluid Temperature Sensor 'A' Circuit", - "P0711": "Transmission Fluid Temperature Sensor 'A' Circuit Range/Performance", - "P0712": "Transmission Fluid Temperature Sensor 'A' Circuit Low", - "P0713": "Transmission Fluid Temperature Sensor 'A' Circuit High", - "P0714": "Transmission Fluid Temperature Sensor 'A' Circuit Intermittent", - "P0715": "Input/Turbine Speed Sensor 'A' Circuit", - "P0716": "Input/Turbine Speed Sensor 'A' Circuit Range/Performance", - "P0717": "Input/Turbine Speed Sensor 'A' Circuit No Signal", - "P0718": "Input/Turbine Speed Sensor 'A' Circuit Intermittent", - "P0719": "Brake Switch 'B' Circuit Low", - "P0720": "Output Speed Sensor Circuit", - "P0721": "Output Speed Sensor Circuit Range/Performance", - "P0722": "Output Speed Sensor Circuit No Signal", - "P0723": "Output Speed Sensor Circuit Intermittent", - "P0724": "Brake Switch 'B' Circuit High", - "P0725": "Engine Speed Input Circuit", - "P0726": "Engine Speed Input Circuit Range/Performance", - "P0727": "Engine Speed Input Circuit No Signal", - "P0728": "Engine Speed Input Circuit Intermittent", - "P0729": "Gear 6 Incorrect Ratio", - "P0730": "Incorrect Gear Ratio", - "P0731": "Gear 1 Incorrect Ratio", - "P0732": "Gear 2 Incorrect Ratio", - "P0733": "Gear 3 Incorrect Ratio", - "P0734": "Gear 4 Incorrect Ratio", - "P0735": "Gear 5 Incorrect Ratio", - "P0736": "Reverse Incorrect Ratio", - "P0737": "TCM Engine Speed Output Circuit", - "P0738": "TCM Engine Speed Output Circuit Low", - "P0739": "TCM Engine Speed Output Circuit High", - "P0740": "Torque Converter Clutch Circuit/Open", - "P0741": "Torque Converter Clutch Circuit Performance or Stuck Off", - "P0742": "Torque Converter Clutch Circuit Stuck On", - "P0743": "Torque Converter Clutch Circuit Electrical", - "P0744": "Torque Converter Clutch Circuit Intermittent", - "P0745": "Pressure Control Solenoid 'A'", - "P0746": "Pressure Control Solenoid 'A' Performance or Stuck Off", - "P0747": "Pressure Control Solenoid 'A' Stuck On", - "P0748": "Pressure Control Solenoid 'A' Electrical", - "P0749": "Pressure Control Solenoid 'A' Intermittent", - "P0750": "Shift Solenoid 'A'", - "P0751": "Shift Solenoid 'A' Performance or Stuck Off", - "P0752": "Shift Solenoid 'A' Stuck On", - "P0753": "Shift Solenoid 'A' Electrical", - "P0754": "Shift Solenoid 'A' Intermittent", - "P0755": "Shift Solenoid 'B'", - "P0756": "Shift Solenoid 'B' Performance or Stuck Off", - "P0757": "Shift Solenoid 'B' Stuck On", - "P0758": "Shift Solenoid 'B' Electrical", - "P0759": "Shift Solenoid 'B' Intermittent", - "P0760": "Shift Solenoid 'C'", - "P0761": "Shift Solenoid 'C' Performance or Stuck Off", - "P0762": "Shift Solenoid 'C' Stuck On", - "P0763": "Shift Solenoid 'C' Electrical", - "P0764": "Shift Solenoid 'C' Intermittent", - "P0765": "Shift Solenoid 'D'", - "P0766": "Shift Solenoid 'D' Performance or Stuck Off", - "P0767": "Shift Solenoid 'D' Stuck On", - "P0768": "Shift Solenoid 'D' Electrical", - "P0769": "Shift Solenoid 'D' Intermittent", - "P0770": "Shift Solenoid 'E'", - "P0771": "Shift Solenoid 'E' Performance or Stuck Off", - "P0772": "Shift Solenoid 'E' Stuck On", - "P0773": "Shift Solenoid 'E' Electrical", - "P0774": "Shift Solenoid 'E' Intermittent", - "P0775": "Pressure Control Solenoid 'B'", - "P0776": "Pressure Control Solenoid 'B' Performance or Stuck off", - "P0777": "Pressure Control Solenoid 'B' Stuck On", - "P0778": "Pressure Control Solenoid 'B' Electrical", - "P0779": "Pressure Control Solenoid 'B' Intermittent", - "P0780": "Shift Error", - "P0781": "1-2 Shift", - "P0782": "2-3 Shift", - "P0783": "3-4 Shift", - "P0784": "4-5 Shift", - "P0785": "Shift/Timing Solenoid", - "P0786": "Shift/Timing Solenoid Range/Performance", - "P0787": "Shift/Timing Solenoid Low", - "P0788": "Shift/Timing Solenoid High", - "P0789": "Shift/Timing Solenoid Intermittent", - "P0790": "Normal/Performance Switch Circuit", - "P0791": "Intermediate Shaft Speed Sensor 'A' Circuit", - "P0792": "Intermediate Shaft Speed Sensor 'A' Circuit Range/Performance", - "P0793": "Intermediate Shaft Speed Sensor 'A' Circuit No Signal", - "P0794": "Intermediate Shaft Speed Sensor 'A' Circuit Intermittent", - "P0795": "Pressure Control Solenoid 'C'", - "P0796": "Pressure Control Solenoid 'C' Performance or Stuck off", - "P0797": "Pressure Control Solenoid 'C' Stuck On", - "P0798": "Pressure Control Solenoid 'C' Electrical", - "P0799": "Pressure Control Solenoid 'C' Intermittent", - "P0800": "Transfer Case Control System (MIL Request)", - "P0801": "Reverse Inhibit Control Circuit", - "P0802": "Transmission Control System MIL Request Circuit/Open", - "P0803": "1-4 Upshift (Skip Shift) Solenoid Control Circuit", - "P0804": "1-4 Upshift (Skip Shift) Lamp Control Circuit", - "P0805": "Clutch Position Sensor Circuit", - "P0806": "Clutch Position Sensor Circuit Range/Performance", - "P0807": "Clutch Position Sensor Circuit Low", - "P0808": "Clutch Position Sensor Circuit High", - "P0809": "Clutch Position Sensor Circuit Intermittent", - "P0810": "Clutch Position Control Error", - "P0811": "Excessive Clutch Slippage", - "P0812": "Reverse Input Circuit", - "P0813": "Reverse Output Circuit", - "P0814": "Transmission Range Display Circuit", - "P0815": "Upshift Switch Circuit", - "P0816": "Downshift Switch Circuit", - "P0817": "Starter Disable Circuit", - "P0818": "Driveline Disconnect Switch Input Circuit", - "P0819": "Up and Down Shift Switch to Transmission Range Correlation", - "P0820": "Gear Lever X-Y Position Sensor Circuit", - "P0821": "Gear Lever X Position Circuit", - "P0822": "Gear Lever Y Position Circuit", - "P0823": "Gear Lever X Position Circuit Intermittent", - "P0824": "Gear Lever Y Position Circuit Intermittent", - "P0825": "Gear Lever Push-Pull Switch (Shift Anticipate)", - "P0826": "Up and Down Shift Switch Circuit", - "P0827": "Up and Down Shift Switch Circuit Low", - "P0828": "Up and Down Shift Switch Circuit High", - "P0829": "5-6 Shift", - "P0830": "Clutch Pedal Switch 'A' Circuit", - "P0831": "Clutch Pedal Switch 'A' Circuit Low", - "P0832": "Clutch Pedal Switch 'A' Circuit High", - "P0833": "Clutch Pedal Switch 'B' Circuit", - "P0834": "Clutch Pedal Switch 'B' Circuit Low", - "P0835": "Clutch Pedal Switch 'B' Circuit High", - "P0836": "Four Wheel Drive (4WD) Switch Circuit", - "P0837": "Four Wheel Drive (4WD) Switch Circuit Range/Performance", - "P0838": "Four Wheel Drive (4WD) Switch Circuit Low", - "P0839": "Four Wheel Drive (4WD) Switch Circuit High", - "P0840": "Transmission Fluid Pressure Sensor/Switch 'A' Circuit", - "P0841": "Transmission Fluid Pressure Sensor/Switch 'A' Circuit Range/Performance", - "P0842": "Transmission Fluid Pressure Sensor/Switch 'A' Circuit Low", - "P0843": "Transmission Fluid Pressure Sensor/Switch 'A' Circuit High", - "P0844": "Transmission Fluid Pressure Sensor/Switch 'A' Circuit Intermittent", - "P0845": "Transmission Fluid Pressure Sensor/Switch 'B' Circuit", - "P0846": "Transmission Fluid Pressure Sensor/Switch 'B' Circuit Range/Performance", - "P0847": "Transmission Fluid Pressure Sensor/Switch 'B' Circuit Low", - "P0848": "Transmission Fluid Pressure Sensor/Switch 'B' Circuit High", - "P0849": "Transmission Fluid Pressure Sensor/Switch 'B' Circuit Intermittent", - "P0850": "Park/Neutral Switch Input Circuit", - "P0851": "Park/Neutral Switch Input Circuit Low", - "P0852": "Park/Neutral Switch Input Circuit High", - "P0853": "Drive Switch Input Circuit", - "P0854": "Drive Switch Input Circuit Low", - "P0855": "Drive Switch Input Circuit High", - "P0856": "Traction Control Input Signal", - "P0857": "Traction Control Input Signal Range/Performance", - "P0858": "Traction Control Input Signal Low", - "P0859": "Traction Control Input Signal High", - "P0860": "Gear Shift Module Communication Circuit", - "P0861": "Gear Shift Module Communication Circuit Low", - "P0862": "Gear Shift Module Communication Circuit High", - "P0863": "TCM Communication Circuit", - "P0864": "TCM Communication Circuit Range/Performance", - "P0865": "TCM Communication Circuit Low", - "P0866": "TCM Communication Circuit High", - "P0867": "Transmission Fluid Pressure", - "P0868": "Transmission Fluid Pressure Low", - "P0869": "Transmission Fluid Pressure High", - "P0870": "Transmission Fluid Pressure Sensor/Switch 'C' Circuit", - "P0871": "Transmission Fluid Pressure Sensor/Switch 'C' Circuit Range/Performance", - "P0872": "Transmission Fluid Pressure Sensor/Switch 'C' Circuit Low", - "P0873": "Transmission Fluid Pressure Sensor/Switch 'C' Circuit High", - "P0874": "Transmission Fluid Pressure Sensor/Switch 'C' Circuit Intermittent", - "P0875": "Transmission Fluid Pressure Sensor/Switch 'D' Circuit", - "P0876": "Transmission Fluid Pressure Sensor/Switch 'D' Circuit Range/Performance", - "P0877": "Transmission Fluid Pressure Sensor/Switch 'D' Circuit Low", - "P0878": "Transmission Fluid Pressure Sensor/Switch 'D' Circuit High", - "P0879": "Transmission Fluid Pressure Sensor/Switch 'D' Circuit Intermittent", - "P0880": "TCM Power Input Signal", - "P0881": "TCM Power Input Signal Range/Performance", - "P0882": "TCM Power Input Signal Low", - "P0883": "TCM Power Input Signal High", - "P0884": "TCM Power Input Signal Intermittent", - "P0885": "TCM Power Relay Control Circuit/Open", - "P0886": "TCM Power Relay Control Circuit Low", - "P0887": "TCM Power Relay Control Circuit High", - "P0888": "TCM Power Relay Sense Circuit", - "P0889": "TCM Power Relay Sense Circuit Range/Performance", - "P0890": "TCM Power Relay Sense Circuit Low", - "P0891": "TCM Power Relay Sense Circuit High", - "P0892": "TCM Power Relay Sense Circuit Intermittent", - "P0893": "Multiple Gears Engaged", - "P0894": "Transmission Component Slipping", - "P0895": "Shift Time Too Short", - "P0896": "Shift Time Too Long", - "P0897": "Transmission Fluid Deteriorated", - "P0898": "Transmission Control System MIL Request Circuit Low", - "P0899": "Transmission Control System MIL Request Circuit High", - "P0900": "Clutch Actuator Circuit/Open", - "P0901": "Clutch Actuator Circuit Range/Performance", - "P0902": "Clutch Actuator Circuit Low", - "P0903": "Clutch Actuator Circuit High", - "P0904": "Gate Select Position Circuit", - "P0905": "Gate Select Position Circuit Range/Performance", - "P0906": "Gate Select Position Circuit Low", - "P0907": "Gate Select Position Circuit High", - "P0908": "Gate Select Position Circuit Intermittent", - "P0909": "Gate Select Control Error", - "P0910": "Gate Select Actuator Circuit/Open", - "P0911": "Gate Select Actuator Circuit Range/Performance", - "P0912": "Gate Select Actuator Circuit Low", - "P0913": "Gate Select Actuator Circuit High", - "P0914": "Gear Shift Position Circuit", - "P0915": "Gear Shift Position Circuit Range/Performance", - "P0916": "Gear Shift Position Circuit Low", - "P0917": "Gear Shift Position Circuit High", - "P0918": "Gear Shift Position Circuit Intermittent", - "P0919": "Gear Shift Position Control Error", - "P0920": "Gear Shift Forward Actuator Circuit/Open", - "P0921": "Gear Shift Forward Actuator Circuit Range/Performance", - "P0922": "Gear Shift Forward Actuator Circuit Low", - "P0923": "Gear Shift Forward Actuator Circuit High", - "P0924": "Gear Shift Reverse Actuator Circuit/Open", - "P0925": "Gear Shift Reverse Actuator Circuit Range/Performance", - "P0926": "Gear Shift Reverse Actuator Circuit Low", - "P0927": "Gear Shift Reverse Actuator Circuit High", - "P0928": "Gear Shift Lock Solenoid Control Circuit/Open", - "P0929": "Gear Shift Lock Solenoid Control Circuit Range/Performance", - "P0930": "Gear Shift Lock Solenoid Control Circuit Low", - "P0931": "Gear Shift Lock Solenoid Control Circuit High", - "P0932": "Hydraulic Pressure Sensor Circuit", - "P0933": "Hydraulic Pressure Sensor Range/Performance", - "P0934": "Hydraulic Pressure Sensor Circuit Low", - "P0935": "Hydraulic Pressure Sensor Circuit High", - "P0936": "Hydraulic Pressure Sensor Circuit Intermittent", - "P0937": "Hydraulic Oil Temperature Sensor Circuit", - "P0938": "Hydraulic Oil Temperature Sensor Range/Performance", - "P0939": "Hydraulic Oil Temperature Sensor Circuit Low", - "P0940": "Hydraulic Oil Temperature Sensor Circuit High", - "P0941": "Hydraulic Oil Temperature Sensor Circuit Intermittent", - "P0942": "Hydraulic Pressure Unit", - "P0943": "Hydraulic Pressure Unit Cycling Period Too Short", - "P0944": "Hydraulic Pressure Unit Loss of Pressure", - "P0945": "Hydraulic Pump Relay Circuit/Open", - "P0946": "Hydraulic Pump Relay Circuit Range/Performance", - "P0947": "Hydraulic Pump Relay Circuit Low", - "P0948": "Hydraulic Pump Relay Circuit High", - "P0949": "Auto Shift Manual Adaptive Learning Not Complete", - "P0950": "Auto Shift Manual Control Circuit", - "P0951": "Auto Shift Manual Control Circuit Range/Performance", - "P0952": "Auto Shift Manual Control Circuit Low", - "P0953": "Auto Shift Manual Control Circuit High", - "P0954": "Auto Shift Manual Control Circuit Intermittent", - "P0955": "Auto Shift Manual Mode Circuit", - "P0956": "Auto Shift Manual Mode Circuit Range/Performance", - "P0957": "Auto Shift Manual Mode Circuit Low", - "P0958": "Auto Shift Manual Mode Circuit High", - "P0959": "Auto Shift Manual Mode Circuit Intermittent", - "P0960": "Pressure Control Solenoid 'A' Control Circuit/Open", - "P0961": "Pressure Control Solenoid 'A' Control Circuit Range/Performance", - "P0962": "Pressure Control Solenoid 'A' Control Circuit Low", - "P0963": "Pressure Control Solenoid 'A' Control Circuit High", - "P0964": "Pressure Control Solenoid 'B' Control Circuit/Open", - "P0965": "Pressure Control Solenoid 'B' Control Circuit Range/Performance", - "P0966": "Pressure Control Solenoid 'B' Control Circuit Low", - "P0967": "Pressure Control Solenoid 'B' Control Circuit High", - "P0968": "Pressure Control Solenoid 'C' Control Circuit/Open", - "P0969": "Pressure Control Solenoid 'C' Control Circuit Range/Performance", - "P0970": "Pressure Control Solenoid 'C' Control Circuit Low", - "P0971": "Pressure Control Solenoid 'C' Control Circuit High", - "P0972": "Shift Solenoid 'A' Control Circuit Range/Performance", - "P0973": "Shift Solenoid 'A' Control Circuit Low", - "P0974": "Shift Solenoid 'A' Control Circuit High", - "P0975": "Shift Solenoid 'B' Control Circuit Range/Performance", - "P0976": "Shift Solenoid 'B' Control Circuit Low", - "P0977": "Shift Solenoid 'B' Control Circuit High", - "P0978": "Shift Solenoid 'C' Control Circuit Range/Performance", - "P0979": "Shift Solenoid 'C' Control Circuit Low", - "P0980": "Shift Solenoid 'C' Control Circuit High", - "P0981": "Shift Solenoid 'D' Control Circuit Range/Performance", - "P0982": "Shift Solenoid 'D' Control Circuit Low", - "P0983": "Shift Solenoid 'D' Control Circuit High", - "P0984": "Shift Solenoid 'E' Control Circuit Range/Performance", - "P0985": "Shift Solenoid 'E' Control Circuit Low", - "P0986": "Shift Solenoid 'E' Control Circuit High", - "P0987": "Transmission Fluid Pressure Sensor/Switch 'E' Circuit", - "P0988": "Transmission Fluid Pressure Sensor/Switch 'E' Circuit Range/Performance", - "P0989": "Transmission Fluid Pressure Sensor/Switch 'E' Circuit Low", - "P0990": "Transmission Fluid Pressure Sensor/Switch 'E' Circuit High", - "P0991": "Transmission Fluid Pressure Sensor/Switch 'E' Circuit Intermittent", - "P0992": "Transmission Fluid Pressure Sensor/Switch 'F' Circuit", - "P0993": "Transmission Fluid Pressure Sensor/Switch 'F' Circuit Range/Performance", - "P0994": "Transmission Fluid Pressure Sensor/Switch 'F' Circuit Low", - "P0995": "Transmission Fluid Pressure Sensor/Switch 'F' Circuit High", - "P0996": "Transmission Fluid Pressure Sensor/Switch 'F' Circuit Intermittent", - "P0997": "Shift Solenoid 'F' Control Circuit Range/Performance", - "P0998": "Shift Solenoid 'F' Control Circuit Low", - "P0999": "Shift Solenoid 'F' Control Circuit High", - "P0A00": "Motor Electronics Coolant Temperature Sensor Circuit", - "P0A01": "Motor Electronics Coolant Temperature Sensor Circuit Range/Performance", - "P0A02": "Motor Electronics Coolant Temperature Sensor Circuit Low", - "P0A03": "Motor Electronics Coolant Temperature Sensor Circuit High", - "P0A04": "Motor Electronics Coolant Temperature Sensor Circuit Intermittent", - "P0A05": "Motor Electronics Coolant Pump Control Circuit/Open", - "P0A06": "Motor Electronics Coolant Pump Control Circuit Low", - "P0A07": "Motor Electronics Coolant Pump Control Circuit High", - "P0A08": "DC/DC Converter Status Circuit", - "P0A09": "DC/DC Converter Status Circuit Low Input", - "P0A10": "DC/DC Converter Status Circuit High Input", - "P0A11": "DC/DC Converter Enable Circuit/Open", - "P0A12": "DC/DC Converter Enable Circuit Low", - "P0A13": "DC/DC Converter Enable Circuit High", - "P0A14": "Engine Mount Control Circuit/Open", - "P0A15": "Engine Mount Control Circuit Low", - "P0A16": "Engine Mount Control Circuit High", - "P0A17": "Motor Torque Sensor Circuit", - "P0A18": "Motor Torque Sensor Circuit Range/Performance", - "P0A19": "Motor Torque Sensor Circuit Low", - "P0A20": "Motor Torque Sensor Circuit High", - "P0A21": "Motor Torque Sensor Circuit Intermittent", - "P0A22": "Generator Torque Sensor Circuit", - "P0A23": "Generator Torque Sensor Circuit Range/Performance", - "P0A24": "Generator Torque Sensor Circuit Low", - "P0A25": "Generator Torque Sensor Circuit High", - "P0A26": "Generator Torque Sensor Circuit Intermittent", - "P0A27": "Battery Power Off Circuit", - "P0A28": "Battery Power Off Circuit Low", - "P0A29": "Battery Power Off Circuit High", - "P2000": "NOx Trap Efficiency Below Threshold", - "P2001": "NOx Trap Efficiency Below Threshold", - "P2002": "Particulate Trap Efficiency Below Threshold", - "P2003": "Particulate Trap Efficiency Below Threshold", - "P2004": "Intake Manifold Runner Control Stuck Open", - "P2005": "Intake Manifold Runner Control Stuck Open", - "P2006": "Intake Manifold Runner Control Stuck Closed", - "P2007": "Intake Manifold Runner Control Stuck Closed", - "P2008": "Intake Manifold Runner Control Circuit/Open", - "P2009": "Intake Manifold Runner Control Circuit Low", - "P2010": "Intake Manifold Runner Control Circuit High", - "P2011": "Intake Manifold Runner Control Circuit/Open", - "P2012": "Intake Manifold Runner Control Circuit Low", - "P2013": "Intake Manifold Runner Control Circuit High", - "P2014": "Intake Manifold Runner Position Sensor/Switch Circuit", - "P2015": "Intake Manifold Runner Position Sensor/Switch Circuit Range/Performance", - "P2016": "Intake Manifold Runner Position Sensor/Switch Circuit Low", - "P2017": "Intake Manifold Runner Position Sensor/Switch Circuit High", - "P2018": "Intake Manifold Runner Position Sensor/Switch Circuit Intermittent", - "P2019": "Intake Manifold Runner Position Sensor/Switch Circuit", - "P2020": "Intake Manifold Runner Position Sensor/Switch Circuit Range/Performance", - "P2021": "Intake Manifold Runner Position Sensor/Switch Circuit Low", - "P2022": "Intake Manifold Runner Position Sensor/Switch Circuit High", - "P2023": "Intake Manifold Runner Position Sensor/Switch Circuit Intermittent", - "P2024": "Evaporative Emissions (EVAP) Fuel Vapor Temperature Sensor Circuit", - "P2025": "Evaporative Emissions (EVAP) Fuel Vapor Temperature Sensor Performance", - "P2026": "Evaporative Emissions (EVAP) Fuel Vapor Temperature Sensor Circuit Low Voltage", - "P2027": "Evaporative Emissions (EVAP) Fuel Vapor Temperature Sensor Circuit High Voltage", - "P2028": "Evaporative Emissions (EVAP) Fuel Vapor Temperature Sensor Circuit Intermittent", - "P2029": "Fuel Fired Heater Disabled", - "P2030": "Fuel Fired Heater Performance", - "P2031": "Exhaust Gas Temperature Sensor Circuit", - "P2032": "Exhaust Gas Temperature Sensor Circuit Low", - "P2033": "Exhaust Gas Temperature Sensor Circuit High", - "P2034": "Exhaust Gas Temperature Sensor Circuit", - "P2035": "Exhaust Gas Temperature Sensor Circuit Low", - "P2036": "Exhaust Gas Temperature Sensor Circuit High", - "P2037": "Reductant Injection Air Pressure Sensor Circuit", - "P2038": "Reductant Injection Air Pressure Sensor Circuit Range/Performance", - "P2039": "Reductant Injection Air Pressure Sensor Circuit Low Input", - "P2040": "Reductant Injection Air Pressure Sensor Circuit High Input", - "P2041": "Reductant Injection Air Pressure Sensor Circuit Intermittent", - "P2042": "Reductant Temperature Sensor Circuit", - "P2043": "Reductant Temperature Sensor Circuit Range/Performance", - "P2044": "Reductant Temperature Sensor Circuit Low Input", - "P2045": "Reductant Temperature Sensor Circuit High Input", - "P2046": "Reductant Temperature Sensor Circuit Intermittent", - "P2047": "Reductant Injector Circuit/Open", - "P2048": "Reductant Injector Circuit Low", - "P2049": "Reductant Injector Circuit High", - "P2050": "Reductant Injector Circuit/Open", - "P2051": "Reductant Injector Circuit Low", - "P2052": "Reductant Injector Circuit High", - "P2053": "Reductant Injector Circuit/Open", - "P2054": "Reductant Injector Circuit Low", - "P2055": "Reductant Injector Circuit High", - "P2056": "Reductant Injector Circuit/Open", - "P2057": "Reductant Injector Circuit Low", - "P2058": "Reductant Injector Circuit High", - "P2059": "Reductant Injection Air Pump Control Circuit/Open", - "P2060": "Reductant Injection Air Pump Control Circuit Low", - "P2061": "Reductant Injection Air Pump Control Circuit High", - "P2062": "Reductant Supply Control Circuit/Open", - "P2063": "Reductant Supply Control Circuit Low", - "P2064": "Reductant Supply Control Circuit High", - "P2065": "Fuel Level Sensor 'B' Circuit", - "P2066": "Fuel Level Sensor 'B' Performance", - "P2067": "Fuel Level Sensor 'B' Circuit Low", - "P2068": "Fuel Level Sensor 'B' Circuit High", - "P2069": "Fuel Level Sensor 'B' Circuit Intermittent", - "P2070": "Intake Manifold Tuning (IMT) Valve Stuck Open", - "P2071": "Intake Manifold Tuning (IMT) Valve Stuck Closed", - "P2075": "Intake Manifold Tuning (IMT) Valve Position Sensor/Switch Circuit", - "P2076": "Intake Manifold Tuning (IMT) Valve Position Sensor/Switch Circuit Range/Performance", - "P2077": "Intake Manifold Tuning (IMT) Valve Position Sensor/Switch Circuit Low", - "P2078": "Intake Manifold Tuning (IMT) Valve Position Sensor/Switch Circuit High", - "P2079": "Intake Manifold Tuning (IMT) Valve Position Sensor/Switch Circuit Intermittent", - "P2080": "Exhaust Gas Temperature Sensor Circuit Range/Performance", - "P2081": "Exhaust Gas Temperature Sensor Circuit Intermittent", - "P2082": "Exhaust Gas Temperature Sensor Circuit Range/Performance", - "P2083": "Exhaust Gas Temperature Sensor Circuit Intermittent", - "P2084": "Exhaust Gas Temperature Sensor Circuit Range/Performance", - "P2085": "Exhaust Gas Temperature Sensor Circuit Intermittent", - "P2086": "Exhaust Gas Temperature Sensor Circuit Range/Performance", - "P2087": "Exhaust Gas Temperature Sensor Circuit Intermittent", - "P2088": "'A' Camshaft Position Actuator Control Circuit Low", - "P2089": "'A' Camshaft Position Actuator Control Circuit High", - "P2090": "'B' Camshaft Position Actuator Control Circuit Low", - "P2091": "'B' Camshaft Position Actuator Control Circuit High", - "P2092": "'A' Camshaft Position Actuator Control Circuit Low", - "P2093": "'A' Camshaft Position Actuator Control Circuit High", - "P2094": "'B' Camshaft Position Actuator Control Circuit Low", - "P2095": "'B' Camshaft Position Actuator Control Circuit High", - "P2096": "Post Catalyst Fuel Trim System Too Lean", - "P2097": "Post Catalyst Fuel Trim System Too Rich", - "P2098": "Post Catalyst Fuel Trim System Too Lean", - "P2099": "Post Catalyst Fuel Trim System Too Rich", - "P2100": "Throttle Actuator Control Motor Circuit/Open", - "P2101": "Throttle Actuator Control Motor Circuit Range/Performance", - "P2102": "Throttle Actuator Control Motor Circuit Low", - "P2103": "Throttle Actuator Control Motor Circuit High", - "P2104": "Throttle Actuator Control System - Forced Idle", - "P2105": "Throttle Actuator Control System - Forced Engine Shutdown", - "P2106": "Throttle Actuator Control System - Forced Limited Power", - "P2107": "Throttle Actuator Control Module Processor", - "P2108": "Throttle Actuator Control Module Performance", - "P2109": "Throttle/Pedal Position Sensor 'A' Minimum Stop Performance", - "P2110": "Throttle Actuator Control System - Forced Limited RPM", - "P2111": "Throttle Actuator Control System - Stuck Open", - "P2112": "Throttle Actuator Control System - Stuck Closed", - "P2113": "Throttle/Pedal Position Sensor 'B' Minimum Stop Performance", - "P2114": "Throttle/Pedal Position Sensor 'C' Minimum Stop Performance", - "P2115": "Throttle/Pedal Position Sensor 'D' Minimum Stop Performance", - "P2116": "Throttle/Pedal Position Sensor 'E' Minimum Stop Performance", - "P2117": "Throttle/Pedal Position Sensor 'F' Minimum Stop Performance", - "P2118": "Throttle Actuator Control Motor Current Range/Performance", - "P2119": "Throttle Actuator Control Throttle Body Range/Performance", - "P2120": "Throttle/Pedal Position Sensor/Switch 'D' Circuit", - "P2121": "Throttle/Pedal Position Sensor/Switch 'D' Circuit Range/Performance", - "P2122": "Throttle/Pedal Position Sensor/Switch 'D' Circuit Low Input", - "P2123": "Throttle/Pedal Position Sensor/Switch 'D' Circuit High Input", - "P2124": "Throttle/Pedal Position Sensor/Switch 'D' Circuit Intermittent", - "P2125": "Throttle/Pedal Position Sensor/Switch 'E' Circuit", - "P2126": "Throttle/Pedal Position Sensor/Switch 'E' Circuit Range/Performance", - "P2127": "Throttle/Pedal Position Sensor/Switch 'E' Circuit Low Input", - "P2128": "Throttle/Pedal Position Sensor/Switch 'E' Circuit High Input", - "P2129": "Throttle/Pedal Position Sensor/Switch 'E' Circuit Intermittent", - "P2130": "Throttle/Pedal Position Sensor/Switch 'F' Circuit", - "P2131": "Throttle/Pedal Position Sensor/Switch 'F' Circuit Range Performance", - "P2132": "Throttle/Pedal Position Sensor/Switch 'F' Circuit Low Input", - "P2133": "Throttle/Pedal Position Sensor/Switch 'F' Circuit High Input", - "P2134": "Throttle/Pedal Position Sensor/Switch 'F' Circuit Intermittent", - "P2135": "Throttle/Pedal Position Sensor/Switch 'A' / 'B' Voltage Correlation", - "P2136": "Throttle/Pedal Position Sensor/Switch 'A' / 'C' Voltage Correlation", - "P2137": "Throttle/Pedal Position Sensor/Switch 'B' / 'C' Voltage Correlation", - "P2138": "Throttle/Pedal Position Sensor/Switch 'D' / 'E' Voltage Correlation", - "P2139": "Throttle/Pedal Position Sensor/Switch 'D' / 'F' Voltage Correlation", - "P2140": "Throttle/Pedal Position Sensor/Switch 'E' / 'F' Voltage Correlation", - "P2141": "Exhaust Gas Recirculation Throttle Control Circuit Low", - "P2142": "Exhaust Gas Recirculation Throttle Control Circuit High", - "P2143": "Exhaust Gas Recirculation Vent Control Circuit/Open", - "P2144": "Exhaust Gas Recirculation Vent Control Circuit Low", - "P2145": "Exhaust Gas Recirculation Vent Control Circuit High", - "P2146": "Fuel Injector Group 'A' Supply Voltage Circuit/Open", - "P2147": "Fuel Injector Group 'A' Supply Voltage Circuit Low", - "P2148": "Fuel Injector Group 'A' Supply Voltage Circuit High", - "P2149": "Fuel Injector Group 'B' Supply Voltage Circuit/Open", - "P2150": "Fuel Injector Group 'B' Supply Voltage Circuit Low", - "P2151": "Fuel Injector Group 'B' Supply Voltage Circuit High", - "P2152": "Fuel Injector Group 'C' Supply Voltage Circuit/Open", - "P2153": "Fuel Injector Group 'C' Supply Voltage Circuit Low", - "P2154": "Fuel Injector Group 'C' Supply Voltage Circuit High", - "P2155": "Fuel Injector Group 'D' Supply Voltage Circuit/Open", - "P2156": "Fuel Injector Group 'D' Supply Voltage Circuit Low", - "P2157": "Fuel Injector Group 'D' Supply Voltage Circuit High", - "P2158": "Vehicle Speed Sensor 'B'", - "P2159": "Vehicle Speed Sensor 'B' Range/Performance", - "P2160": "Vehicle Speed Sensor 'B' Circuit Low", - "P2161": "Vehicle Speed Sensor 'B' Intermittent/Erratic", - "P2162": "Vehicle Speed Sensor 'A' / 'B' Correlation", - "P2163": "Throttle/Pedal Position Sensor 'A' Maximum Stop Performance", - "P2164": "Throttle/Pedal Position Sensor 'B' Maximum Stop Performance", - "P2165": "Throttle/Pedal Position Sensor 'C' Maximum Stop Performance", - "P2166": "Throttle/Pedal Position Sensor 'D' Maximum Stop Performance", - "P2167": "Throttle/Pedal Position Sensor 'E' Maximum Stop Performance", - "P2168": "Throttle/Pedal Position Sensor 'F' Maximum Stop Performance", - "P2169": "Exhaust Pressure Regulator Vent Solenoid Control Circuit/Open", - "P2170": "Exhaust Pressure Regulator Vent Solenoid Control Circuit Low", - "P2171": "Exhaust Pressure Regulator Vent Solenoid Control Circuit High", - "P2172": "Throttle Actuator Control System - Sudden High Airflow Detected", - "P2173": "Throttle Actuator Control System - High Airflow Detected", - "P2174": "Throttle Actuator Control System - Sudden Low Airflow Detected", - "P2175": "Throttle Actuator Control System - Low Airflow Detected", - "P2176": "Throttle Actuator Control System - Idle Position Not Learned", - "P2177": "System Too Lean Off Idle", - "P2178": "System Too Rich Off Idle", - "P2179": "System Too Lean Off Idle", - "P2180": "System Too Rich Off Idle", - "P2181": "Cooling System Performance", - "P2182": "Engine Coolant Temperature Sensor 2 Circuit", - "P2183": "Engine Coolant Temperature Sensor 2 Circuit Range/Performance", - "P2184": "Engine Coolant Temperature Sensor 2 Circuit Low", - "P2185": "Engine Coolant Temperature Sensor 2 Circuit High", - "P2186": "Engine Coolant Temperature Sensor 2 Circuit Intermittent/Erratic", - "P2187": "System Too Lean at Idle", - "P2188": "System Too Rich at Idle", - "P2189": "System Too Lean at Idle", - "P2190": "System Too Rich at Idle", - "P2191": "System Too Lean at Higher Load", - "P2192": "System Too Rich at Higher Load", - "P2193": "System Too Lean at Higher Load", - "P2194": "System Too Rich at Higher Load", - "P2195": "O2 Sensor Signal Stuck Lean", - "P2196": "O2 Sensor Signal Stuck Rich", - "P2197": "O2 Sensor Signal Stuck Lean", - "P2198": "O2 Sensor Signal Stuck Rich", - "P2199": "Intake Air Temperature Sensor 1 / 2 Correlation", - "P2200": "NOx Sensor Circuit", - "P2201": "NOx Sensor Circuit Range/Performance", - "P2202": "NOx Sensor Circuit Low Input", - "P2203": "NOx Sensor Circuit High Input", - "P2204": "NOx Sensor Circuit Intermittent Input", - "P2205": "NOx Sensor Heater Control Circuit/Open", - "P2206": "NOx Sensor Heater Control Circuit Low", - "P2207": "NOx Sensor Heater Control Circuit High", - "P2208": "NOx Sensor Heater Sense Circuit", - "P2209": "NOx Sensor Heater Sense Circuit Range/Performance", - "P2210": "NOx Sensor Heater Sense Circuit Low Input", - "P2211": "NOx Sensor Heater Sense Circuit High Input", - "P2212": "NOx Sensor Heater Sense Circuit Intermittent", - "P2213": "NOx Sensor Circuit", - "P2214": "NOx Sensor Circuit Range/Performance", - "P2215": "NOx Sensor Circuit Low Input", - "P2216": "NOx Sensor Circuit High Input", - "P2217": "NOx Sensor Circuit Intermittent Input", - "P2218": "NOx Sensor Heater Control Circuit/Open", - "P2219": "NOx Sensor Heater Control Circuit Low", - "P2220": "NOx Sensor Heater Control Circuit High", - "P2221": "NOx Sensor Heater Sense Circuit", - "P2222": "NOx Sensor Heater Sense Circuit Range/Performance", - "P2223": "NOx Sensor Heater Sense Circuit Low", - "P2224": "NOx Sensor Heater Sense Circuit High", - "P2225": "NOx Sensor Heater Sense Circuit Intermittent", - "P2226": "Barometric Pressure Circuit", - "P2227": "Barometric Pressure Circuit Range/Performance", - "P2228": "Barometric Pressure Circuit Low", - "P2229": "Barometric Pressure Circuit High", - "P2230": "Barometric Pressure Circuit Intermittent", - "P2231": "O2 Sensor Signal Circuit Shorted to Heater Circuit", - "P2232": "O2 Sensor Signal Circuit Shorted to Heater Circuit", - "P2233": "O2 Sensor Signal Circuit Shorted to Heater Circuit", - "P2234": "O2 Sensor Signal Circuit Shorted to Heater Circuit", - "P2235": "O2 Sensor Signal Circuit Shorted to Heater Circuit", - "P2236": "O2 Sensor Signal Circuit Shorted to Heater Circuit", - "P2237": "O2 Sensor Positive Current Control Circuit/Open", - "P2238": "O2 Sensor Positive Current Control Circuit Low", - "P2239": "O2 Sensor Positive Current Control Circuit High", - "P2240": "O2 Sensor Positive Current Control Circuit/Open", - "P2241": "O2 Sensor Positive Current Control Circuit Low", - "P2242": "O2 Sensor Positive Current Control Circuit High", - "P2243": "O2 Sensor Reference Voltage Circuit/Open", - "P2244": "O2 Sensor Reference Voltage Performance", - "P2245": "O2 Sensor Reference Voltage Circuit Low", - "P2246": "O2 Sensor Reference Voltage Circuit High", - "P2247": "O2 Sensor Reference Voltage Circuit/Open", - "P2248": "O2 Sensor Reference Voltage Performance", - "P2249": "O2 Sensor Reference Voltage Circuit Low", - "P2250": "O2 Sensor Reference Voltage Circuit High", - "P2251": "O2 Sensor Negative Current Control Circuit/Open", - "P2252": "O2 Sensor Negative Current Control Circuit Low", - "P2253": "O2 Sensor Negative Current Control Circuit High", - "P2254": "O2 Sensor Negative Current Control Circuit/Open", - "P2255": "O2 Sensor Negative Current Control Circuit Low", - "P2256": "O2 Sensor Negative Current Control Circuit High", - "P2257": "Secondary Air Injection System Control 'A' Circuit Low", - "P2258": "Secondary Air Injection System Control 'A' Circuit High", - "P2259": "Secondary Air Injection System Control 'B' Circuit Low", - "P2260": "Secondary Air Injection System Control 'B' Circuit High", - "P2261": "Turbo/Super Charger Bypass Valve - Mechanical", - "P2262": "Turbo Boost Pressure Not Detected - Mechanical", - "P2263": "Turbo/Super Charger Boost System Performance", - "P2264": "Water in Fuel Sensor Circuit", - "P2265": "Water in Fuel Sensor Circuit Range/Performance", - "P2266": "Water in Fuel Sensor Circuit Low", - "P2267": "Water in Fuel Sensor Circuit High", - "P2268": "Water in Fuel Sensor Circuit Intermittent", - "P2269": "Water in Fuel Condition", - "P2270": "O2 Sensor Signal Stuck Lean", - "P2271": "O2 Sensor Signal Stuck Rich", - "P2272": "O2 Sensor Signal Stuck Lean", - "P2273": "O2 Sensor Signal Stuck Rich", - "P2274": "O2 Sensor Signal Stuck Lean", - "P2275": "O2 Sensor Signal Stuck Rich", - "P2276": "O2 Sensor Signal Stuck Lean", - "P2277": "O2 Sensor Signal Stuck Rich", - "P2278": "O2 Sensor Signals Swapped Bank 1 Sensor 3 / Bank 2 Sensor 3", - "P2279": "Intake Air System Leak", - "P2280": "Air Flow Restriction / Air Leak Between Air Filter and MAF", - "P2281": "Air Leak Between MAF and Throttle Body", - "P2282": "Air Leak Between Throttle Body and Intake Valves", - "P2283": "Injector Control Pressure Sensor Circuit", - "P2284": "Injector Control Pressure Sensor Circuit Range/Performance", - "P2285": "Injector Control Pressure Sensor Circuit Low", - "P2286": "Injector Control Pressure Sensor Circuit High", - "P2287": "Injector Control Pressure Sensor Circuit Intermittent", - "P2288": "Injector Control Pressure Too High", - "P2289": "Injector Control Pressure Too High - Engine Off", - "P2290": "Injector Control Pressure Too Low", - "P2291": "Injector Control Pressure Too Low - Engine Cranking", - "P2292": "Injector Control Pressure Erratic", - "P2293": "Fuel Pressure Regulator 2 Performance", - "P2294": "Fuel Pressure Regulator 2 Control Circuit", - "P2295": "Fuel Pressure Regulator 2 Control Circuit Low", - "P2296": "Fuel Pressure Regulator 2 Control Circuit High", - "P2297": "O2 Sensor Out of Range During Deceleration", - "P2298": "O2 Sensor Out of Range During Deceleration", - "P2299": "Brake Pedal Position / Accelerator Pedal Position Incompatible", - "P2300": "Ignition Coil 'A' Primary Control Circuit Low", - "P2301": "Ignition Coil 'A' Primary Control Circuit High", - "P2302": "Ignition Coil 'A' Secondary Circuit", - "P2303": "Ignition Coil 'B' Primary Control Circuit Low", - "P2304": "Ignition Coil 'B' Primary Control Circuit High", - "P2305": "Ignition Coil 'B' Secondary Circuit", - "P2306": "Ignition Coil 'C' Primary Control Circuit Low", - "P2307": "Ignition Coil 'C' Primary Control Circuit High", - "P2308": "Ignition Coil 'C' Secondary Circuit", - "P2309": "Ignition Coil 'D' Primary Control Circuit Low", - "P2310": "Ignition Coil 'D' Primary Control Circuit High", - "P2311": "Ignition Coil 'D' Secondary Circuit", - "P2312": "Ignition Coil 'E' Primary Control Circuit Low", - "P2313": "Ignition Coil 'E' Primary Control Circuit High", - "P2314": "Ignition Coil 'E' Secondary Circuit", - "P2315": "Ignition Coil 'F' Primary Control Circuit Low", - "P2316": "Ignition Coil 'F' Primary Control Circuit High", - "P2317": "Ignition Coil 'F' Secondary Circuit", - "P2318": "Ignition Coil 'G' Primary Control Circuit Low", - "P2319": "Ignition Coil 'G' Primary Control Circuit High", - "P2320": "Ignition Coil 'G' Secondary Circuit", - "P2321": "Ignition Coil 'H' Primary Control Circuit Low", - "P2322": "Ignition Coil 'H' Primary Control Circuit High", - "P2323": "Ignition Coil 'H' Secondary Circuit", - "P2324": "Ignition Coil 'I' Primary Control Circuit Low", - "P2325": "Ignition Coil 'I' Primary Control Circuit High", - "P2326": "Ignition Coil 'I' Secondary Circuit", - "P2327": "Ignition Coil 'J' Primary Control Circuit Low", - "P2328": "Ignition Coil 'J' Primary Control Circuit High", - "P2329": "Ignition Coil 'J' Secondary Circuit", - "P2330": "Ignition Coil 'K' Primary Control Circuit Low", - "P2331": "Ignition Coil 'K' Primary Control Circuit High", - "P2332": "Ignition Coil 'K' Secondary Circuit", - "P2333": "Ignition Coil 'L' Primary Control Circuit Low", - "P2334": "Ignition Coil 'L' Primary Control Circuit High", - "P2335": "Ignition Coil 'L' Secondary Circuit", - "P2336": "Cylinder #1 Above Knock Threshold", - "P2337": "Cylinder #2 Above Knock Threshold", - "P2338": "Cylinder #3 Above Knock Threshold", - "P2339": "Cylinder #4 Above Knock Threshold", - "P2340": "Cylinder #5 Above Knock Threshold", - "P2341": "Cylinder #6 Above Knock Threshold", - "P2342": "Cylinder #7 Above Knock Threshold", - "P2343": "Cylinder #8 Above Knock Threshold", - "P2344": "Cylinder #9 Above Knock Threshold", - "P2345": "Cylinder #10 Above Knock Threshold", - "P2346": "Cylinder #11 Above Knock Threshold", - "P2347": "Cylinder #12 Above Knock Threshold", - "P2400": "Evaporative Emission System Leak Detection Pump Control Circuit/Open", - "P2401": "Evaporative Emission System Leak Detection Pump Control Circuit Low", - "P2402": "Evaporative Emission System Leak Detection Pump Control Circuit High", - "P2403": "Evaporative Emission System Leak Detection Pump Sense Circuit/Open", - "P2404": "Evaporative Emission System Leak Detection Pump Sense Circuit Range/Performance", - "P2405": "Evaporative Emission System Leak Detection Pump Sense Circuit Low", - "P2406": "Evaporative Emission System Leak Detection Pump Sense Circuit High", - "P2407": "Evaporative Emission System Leak Detection Pump Sense Circuit Intermittent/Erratic", - "P2408": "Fuel Cap Sensor/Switch Circuit", - "P2409": "Fuel Cap Sensor/Switch Circuit Range/Performance", - "P2410": "Fuel Cap Sensor/Switch Circuit Low", - "P2411": "Fuel Cap Sensor/Switch Circuit High", - "P2412": "Fuel Cap Sensor/Switch Circuit Intermittent/Erratic", - "P2413": "Exhaust Gas Recirculation System Performance", - "P2414": "O2 Sensor Exhaust Sample Error", - "P2415": "O2 Sensor Exhaust Sample Error", - "P2416": "O2 Sensor Signals Swapped Bank 1 Sensor 2 / Bank 1 Sensor 3", - "P2417": "O2 Sensor Signals Swapped Bank 2 Sensor 2 / Bank 2 Sensor 3", - "P2418": "Evaporative Emission System Switching Valve Control Circuit / Open", - "P2419": "Evaporative Emission System Switching Valve Control Circuit Low", - "P2420": "Evaporative Emission System Switching Valve Control Circuit High", - "P2421": "Evaporative Emission System Vent Valve Stuck Open", - "P2422": "Evaporative Emission System Vent Valve Stuck Closed", - "P2423": "HC Adsorption Catalyst Efficiency Below Threshold", - "P2424": "HC Adsorption Catalyst Efficiency Below Threshold", - "P2425": "Exhaust Gas Recirculation Cooling Valve Control Circuit/Open", - "P2426": "Exhaust Gas Recirculation Cooling Valve Control Circuit Low", - "P2427": "Exhaust Gas Recirculation Cooling Valve Control Circuit High", - "P2428": "Exhaust Gas Temperature Too High", - "P2429": "Exhaust Gas Temperature Too High", - "P2430": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit", - "P2431": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit Range/Performance", - "P2432": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit Low", - "P2433": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit High", - "P2434": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit Intermittent/Erratic", - "P2435": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit", - "P2436": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit Range/Performance", - "P2437": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit Low", - "P2438": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit High", - "P2439": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit Intermittent/Erratic", - "P2440": "Secondary Air Injection System Switching Valve Stuck Open", - "P2441": "Secondary Air Injection System Switching Valve Stuck Closed", - "P2442": "Secondary Air Injection System Switching Valve Stuck Open", - "P2443": "Secondary Air Injection System Switching Valve Stuck Closed", - "P2444": "Secondary Air Injection System Pump Stuck On", - "P2445": "Secondary Air Injection System Pump Stuck Off", - "P2446": "Secondary Air Injection System Pump Stuck On", - "P2447": "Secondary Air Injection System Pump Stuck Off", - "P2500": "Generator Lamp/L-Terminal Circuit Low", - "P2501": "Generator Lamp/L-Terminal Circuit High", - "P2502": "Charging System Voltage", - "P2503": "Charging System Voltage Low", - "P2504": "Charging System Voltage High", - "P2505": "ECM/PCM Power Input Signal", - "P2506": "ECM/PCM Power Input Signal Range/Performance", - "P2507": "ECM/PCM Power Input Signal Low", - "P2508": "ECM/PCM Power Input Signal High", - "P2509": "ECM/PCM Power Input Signal Intermittent", - "P2510": "ECM/PCM Power Relay Sense Circuit Range/Performance", - "P2511": "ECM/PCM Power Relay Sense Circuit Intermittent", - "P2512": "Event Data Recorder Request Circuit/ Open", - "P2513": "Event Data Recorder Request Circuit Low", - "P2514": "Event Data Recorder Request Circuit High", - "P2515": "A/C Refrigerant Pressure Sensor 'B' Circuit", - "P2516": "A/C Refrigerant Pressure Sensor 'B' Circuit Range/Performance", - "P2517": "A/C Refrigerant Pressure Sensor 'B' Circuit Low", - "P2518": "A/C Refrigerant Pressure Sensor 'B' Circuit High", - "P2519": "A/C Request 'A' Circuit", - "P2520": "A/C Request 'A' Circuit Low", - "P2521": "A/C Request 'A' Circuit High", - "P2522": "A/C Request 'B' Circuit", - "P2523": "A/C Request 'B' Circuit Low", - "P2524": "A/C Request 'B' Circuit High", - "P2525": "Vacuum Reservoir Pressure Sensor Circuit", - "P2526": "Vacuum Reservoir Pressure Sensor Circuit Range/Performance", - "P2527": "Vacuum Reservoir Pressure Sensor Circuit Low", - "P2528": "Vacuum Reservoir Pressure Sensor Circuit High", - "P2529": "Vacuum Reservoir Pressure Sensor Circuit Intermittent", - "P2530": "Ignition Switch Run Position Circuit", - "P2531": "Ignition Switch Run Position Circuit Low", - "P2532": "Ignition Switch Run Position Circuit High", - "P2533": "Ignition Switch Run/Start Position Circuit", - "P2534": "Ignition Switch Run/Start Position Circuit Low", - "P2535": "Ignition Switch Run/Start Position Circuit High", - "P2536": "Ignition Switch Accessory Position Circuit", - "P2537": "Ignition Switch Accessory Position Circuit Low", - "P2538": "Ignition Switch Accessory Position Circuit High", - "P2539": "Low Pressure Fuel System Sensor Circuit", - "P2540": "Low Pressure Fuel System Sensor Circuit Range/Performance", - "P2541": "Low Pressure Fuel System Sensor Circuit Low", - "P2542": "Low Pressure Fuel System Sensor Circuit High", - "P2543": "Low Pressure Fuel System Sensor Circuit Intermittent", - "P2544": "Torque Management Request Input Signal 'A'", - "P2545": "Torque Management Request Input Signal 'A' Range/Performance", - "P2546": "Torque Management Request Input Signal 'A' Low", - "P2547": "Torque Management Request Input Signal 'A' High", - "P2548": "Torque Management Request Input Signal 'B'", - "P2549": "Torque Management Request Input Signal 'B' Range/Performance", - "P2550": "Torque Management Request Input Signal 'B' Low", - "P2551": "Torque Management Request Input Signal 'B' High", - "P2552": "Throttle/Fuel Inhibit Circuit", - "P2553": "Throttle/Fuel Inhibit Circuit Range/Performance", - "P2554": "Throttle/Fuel Inhibit Circuit Low", - "P2555": "Throttle/Fuel Inhibit Circuit High", - "P2556": "Engine Coolant Level Sensor/Switch Circuit", - "P2557": "Engine Coolant Level Sensor/Switch Circuit Range/Performance", - "P2558": "Engine Coolant Level Sensor/Switch Circuit Low", - "P2559": "Engine Coolant Level Sensor/Switch Circuit High", - "P2560": "Engine Coolant Level Low", - "P2561": "A/C Control Module Requested MIL Illumination", - "P2562": "Turbocharger Boost Control Position Sensor Circuit", - "P2563": "Turbocharger Boost Control Position Sensor Circuit Range/Performance", - "P2564": "Turbocharger Boost Control Position Sensor Circuit Low", - "P2565": "Turbocharger Boost Control Position Sensor Circuit High", - "P2566": "Turbocharger Boost Control Position Sensor Circuit Intermittent", - "P2567": "Direct Ozone Reduction Catalyst Temperature Sensor Circuit", - "P2568": "Direct Ozone Reduction Catalyst Temperature Sensor Circuit Range/Performance", - "P2569": "Direct Ozone Reduction Catalyst Temperature Sensor Circuit Low", - "P2570": "Direct Ozone Reduction Catalyst Temperature Sensor Circuit High", - "P2571": "Direct Ozone Reduction Catalyst Temperature Sensor Circuit Intermittent/Erratic", - "P2572": "Direct Ozone Reduction Catalyst Deterioration Sensor Circuit", - "P2573": "Direct Ozone Reduction Catalyst Deterioration Sensor Circuit Range/Performance", - "P2574": "Direct Ozone Reduction Catalyst Deterioration Sensor Circuit Low", - "P2575": "Direct Ozone Reduction Catalyst Deterioration Sensor Circuit High", - "P2576": "Direct Ozone Reduction Catalyst Deterioration Sensor Circuit Intermittent/Erratic", - "P2577": "Direct Ozone Reduction Catalyst Efficiency Below Threshold", - "P2600": "Coolant Pump Control Circuit/Open", - "P2601": "Coolant Pump Control Circuit Range/Performance", - "P2602": "Coolant Pump Control Circuit Low", - "P2603": "Coolant Pump Control Circuit High", - "P2604": "Intake Air Heater 'A' Circuit Range/Performance", - "P2605": "Intake Air Heater 'A' Circuit/Open", - "P2606": "Intake Air Heater 'B' Circuit Range/Performance", - "P2607": "Intake Air Heater 'B' Circuit Low", - "P2608": "Intake Air Heater 'B' Circuit High", - "P2609": "Intake Air Heater System Performance", - "P2610": "ECM/PCM Internal Engine Off Timer Performance", - "P2611": "A/C Refrigerant Distribution Valve Control Circuit/Open", - "P2612": "A/C Refrigerant Distribution Valve Control Circuit Low", - "P2613": "A/C Refrigerant Distribution Valve Control Circuit High", - "P2614": "Camshaft Position Signal Output Circuit/Open", - "P2615": "Camshaft Position Signal Output Circuit Low", - "P2616": "Camshaft Position Signal Output Circuit High", - "P2617": "Crankshaft Position Signal Output Circuit/Open", - "P2618": "Crankshaft Position Signal Output Circuit Low", - "P2619": "Crankshaft Position Signal Output Circuit High", - "P2620": "Throttle Position Output Circuit/Open", - "P2621": "Throttle Position Output Circuit Low", - "P2622": "Throttle Position Output Circuit High", - "P2623": "Injector Control Pressure Regulator Circuit/Open", - "P2624": "Injector Control Pressure Regulator Circuit Low", - "P2625": "Injector Control Pressure Regulator Circuit High", - "P2626": "O2 Sensor Pumping Current Trim Circuit/Open", - "P2627": "O2 Sensor Pumping Current Trim Circuit Low", - "P2628": "O2 Sensor Pumping Current Trim Circuit High", - "P2629": "O2 Sensor Pumping Current Trim Circuit/Open", - "P2630": "O2 Sensor Pumping Current Trim Circuit Low", - "P2631": "O2 Sensor Pumping Current Trim Circuit High", - "P2632": "Fuel Pump 'B' Control Circuit /Open", - "P2633": "Fuel Pump 'B' Control Circuit Low", - "P2634": "Fuel Pump 'B' Control Circuit High", - "P2635": "Fuel Pump 'A' Low Flow / Performance", - "P2636": "Fuel Pump 'B' Low Flow / Performance", - "P2637": "Torque Management Feedback Signal 'A'", - "P2638": "Torque Management Feedback Signal 'A' Range/Performance", - "P2639": "Torque Management Feedback Signal 'A' Low", - "P2640": "Torque Management Feedback Signal 'A' High", - "P2641": "Torque Management Feedback Signal 'B'", - "P2642": "Torque Management Feedback Signal 'B' Range/Performance", - "P2643": "Torque Management Feedback Signal 'B' Low", - "P2644": "Torque Management Feedback Signal 'B' High", - "P2645": "'A' Rocker Arm Actuator Control Circuit/Open", - "P2646": "'A' Rocker Arm Actuator System Performance or Stuck Off", - "P2647": "'A' Rocker Arm Actuator System Stuck On", - "P2648": "'A' Rocker Arm Actuator Control Circuit Low", - "P2649": "'A' Rocker Arm Actuator Control Circuit High", - "P2650": "'B' Rocker Arm Actuator Control Circuit/Open", - "P2651": "'B' Rocker Arm Actuator System Performance or Stuck Off", - "P2652": "'B' Rocker Arm Actuator System Stuck On", - "P2653": "'B' Rocker Arm Actuator Control Circuit Low", - "P2654": "'B' Rocker Arm Actuator Control Circuit High", - "P2655": "'A' Rocker Arm Actuator Control Circuit/Open", - "P2656": "'A' Rocker Arm Actuator System Performance or Stuck Off", - "P2657": "'A' Rocker Arm Actuator System Stuck On", - "P2658": "'A' Rocker Arm Actuator Control Circuit Low", - "P2659": "'A' Rocker Arm Actuator Control Circuit High", - "P2660": "'B' Rocker Arm Actuator Control Circuit/Open", - "P2661": "'B' Rocker Arm Actuator System Performance or Stuck Off", - "P2662": "'B' Rocker Arm Actuator System Stuck On", - "P2663": "'B' Rocker Arm Actuator Control Circuit Low", - "P2664": "'B' Rocker Arm Actuator Control Circuit High", - "P2665": "Fuel Shutoff Valve 'B' Control Circuit/Open", - "P2666": "Fuel Shutoff Valve 'B' Control Circuit Low", - "P2667": "Fuel Shutoff Valve 'B' Control Circuit High", - "P2668": "Fuel Mode Indicator Lamp Control Circuit", - "P2669": "Actuator Supply Voltage 'B' Circuit /Open", - "P2670": "Actuator Supply Voltage 'B' Circuit Low", - "P2671": "Actuator Supply Voltage 'B' Circuit High", - "P2700": "Transmission Friction Element 'A' Apply Time Range/Performance", - "P2701": "Transmission Friction Element 'B' Apply Time Range/Performance", - "P2702": "Transmission Friction Element 'C' Apply Time Range/Performance", - "P2703": "Transmission Friction Element 'D' Apply Time Range/Performance", - "P2704": "Transmission Friction Element 'E' Apply Time Range/Performance", - "P2705": "Transmission Friction Element 'F' Apply Time Range/Performance", - "P2706": "Shift Solenoid 'F'", - "P2707": "Shift Solenoid 'F' Performance or Stuck Off", - "P2708": "Shift Solenoid 'F' Stuck On", - "P2709": "Shift Solenoid 'F' Electrical", - "P2710": "Shift Solenoid 'F' Intermittent", - "P2711": "Unexpected Mechanical Gear Disengagement", - "P2712": "Hydraulic Power Unit Leakage", - "P2713": "Pressure Control Solenoid 'D'", - "P2714": "Pressure Control Solenoid 'D' Performance or Stuck Off", - "P2715": "Pressure Control Solenoid 'D' Stuck On", - "P2716": "Pressure Control Solenoid 'D' Electrical", - "P2717": "Pressure Control Solenoid 'D' Intermittent", - "P2718": "Pressure Control Solenoid 'D' Control Circuit / Open", - "P2719": "Pressure Control Solenoid 'D' Control Circuit Range/Performance", - "P2720": "Pressure Control Solenoid 'D' Control Circuit Low", - "P2721": "Pressure Control Solenoid 'D' Control Circuit High", - "P2722": "Pressure Control Solenoid 'E'", - "P2723": "Pressure Control Solenoid 'E' Performance or Stuck Off", - "P2724": "Pressure Control Solenoid 'E' Stuck On", - "P2725": "Pressure Control Solenoid 'E' Electrical", - "P2726": "Pressure Control Solenoid 'E' Intermittent", - "P2727": "Pressure Control Solenoid 'E' Control Circuit / Open", - "P2728": "Pressure Control Solenoid 'E' Control Circuit Range/Performance", - "P2729": "Pressure Control Solenoid 'E' Control Circuit Low", - "P2730": "Pressure Control Solenoid 'E' Control Circuit High", - "P2731": "Pressure Control Solenoid 'F'", - "P2732": "Pressure Control Solenoid 'F' Performance or Stuck Off", - "P2733": "Pressure Control Solenoid 'F' Stuck On", - "P2734": "Pressure Control Solenoid 'F' Electrical", - "P2735": "Pressure Control Solenoid 'F' Intermittent", - "P2736": "Pressure Control Solenoid 'F' Control Circuit/Open", - "P2737": "Pressure Control Solenoid 'F' Control Circuit Range/Performance", - "P2738": "Pressure Control Solenoid 'F' Control Circuit Low", - "P2739": "Pressure Control Solenoid 'F' Control Circuit High", - "P2740": "Transmission Fluid Temperature Sensor 'B' Circuit", - "P2741": "Transmission Fluid Temperature Sensor 'B' Circuit Range Performance", - "P2742": "Transmission Fluid Temperature Sensor 'B' Circuit Low", - "P2743": "Transmission Fluid Temperature Sensor 'B' Circuit High", - "P2744": "Transmission Fluid Temperature Sensor 'B' Circuit Intermittent", - "P2745": "Intermediate Shaft Speed Sensor 'B' Circuit", - "P2746": "Intermediate Shaft Speed Sensor 'B' Circuit Range/Performance", - "P2747": "Intermediate Shaft Speed Sensor 'B' Circuit No Signal", - "P2748": "Intermediate Shaft Speed Sensor 'B' Circuit Intermittent", - "P2749": "Intermediate Shaft Speed Sensor 'C' Circuit", - "P2750": "Intermediate Shaft Speed Sensor 'C' Circuit Range/Performance", - "P2751": "Intermediate Shaft Speed Sensor 'C' Circuit No Signal", - "P2752": "Intermediate Shaft Speed Sensor 'C' Circuit Intermittent", - "P2753": "Transmission Fluid Cooler Control Circuit/Open", - "P2754": "Transmission Fluid Cooler Control Circuit Low", - "P2755": "Transmission Fluid Cooler Control Circuit High", - "P2756": "Torque Converter Clutch Pressure Control Solenoid", - "P2757": "Torque Converter Clutch Pressure Control Solenoid Control Circuit Performance or Stuck Off", - "P2758": "Torque Converter Clutch Pressure Control Solenoid Control Circuit Stuck On", - "P2759": "Torque Converter Clutch Pressure Control Solenoid Control Circuit Electrical", - "P2760": "Torque Converter Clutch Pressure Control Solenoid Control Circuit Intermittent", - "P2761": "Torque Converter Clutch Pressure Control Solenoid Control Circuit/Open", - "P2762": "Torque Converter Clutch Pressure Control Solenoid Control Circuit Range/Performance", - "P2763": "Torque Converter Clutch Pressure Control Solenoid Control Circuit High", - "P2764": "Torque Converter Clutch Pressure Control Solenoid Control Circuit Low", - "P2765": "Input/Turbine Speed Sensor 'B' Circuit", - "P2766": "Input/Turbine Speed Sensor 'B' Circuit Range/Performance", - "P2767": "Input/Turbine Speed Sensor 'B' Circuit No Signal", - "P2768": "Input/Turbine Speed Sensor 'B' Circuit Intermittent", - "P2769": "Torque Converter Clutch Circuit Low", - "P2770": "Torque Converter Clutch Circuit High", - "P2771": "Four Wheel Drive (4WD) Low Switch Circuit", - "P2772": "Four Wheel Drive (4WD) Low Switch Circuit Range/Performance", - "P2773": "Four Wheel Drive (4WD) Low Switch Circuit Low", - "P2774": "Four Wheel Drive (4WD) Low Switch Circuit High", - "P2775": "Upshift Switch Circuit Range/Performance", - "P2776": "Upshift Switch Circuit Low", - "P2777": "Upshift Switch Circuit High", - "P2778": "Upshift Switch Circuit Intermittent/Erratic", - "P2779": "Downshift Switch Circuit Range/Performance", - "P2780": "Downshift Switch Circuit Low", - "P2781": "Downshift Switch Circuit High", - "P2782": "Downshift Switch Circuit Intermittent/Erratic", - "P2783": "Torque Converter Temperature Too High", - "P2784": "Input/Turbine Speed Sensor 'A'/'B' Correlation", - "P2785": "Clutch Actuator Temperature Too High", - "P2786": "Gear Shift Actuator Temperature Too High", - "P2787": "Clutch Temperature Too High", - "P2788": "Auto Shift Manual Adaptive Learning at Limit", - "P2789": "Clutch Adaptive Learning at Limit", - "P2790": "Gate Select Direction Circuit", - "P2791": "Gate Select Direction Circuit Low", - "P2792": "Gate Select Direction Circuit High", - "P2793": "Gear Shift Direction Circuit", - "P2794": "Gear Shift Direction Circuit Low", - "P2795": "Gear Shift Direction Circuit High", - "P2A00": "O2 Sensor Circuit Range/Performance", - "P2A01": "O2 Sensor Circuit Range/Performance", - "P2A02": "O2 Sensor Circuit Range/Performance", - "P2A03": "O2 Sensor Circuit Range/Performance", - "P2A04": "O2 Sensor Circuit Range/Performance", - "P2A05": "O2 Sensor Circuit Range/Performance", - "P3400": "Cylinder Deactivation System", - "P3401": "Cylinder 1 Deactivation/lntake Valve Control Circuit/Open", - "P3402": "Cylinder 1 Deactivation/lntake Valve Control Performance", - "P3403": "Cylinder 1 Deactivation/lntake Valve Control Circuit Low", - "P3404": "Cylinder 1 Deactivation/lntake Valve Control Circuit High", - "P3405": "Cylinder 1 Exhaust Valve Control Circuit/Open", - "P3406": "Cylinder 1 Exhaust Valve Control Performance", - "P3407": "Cylinder 1 Exhaust Valve Control Circuit Low", - "P3408": "Cylinder 1 Exhaust Valve Control Circuit High", - "P3409": "Cylinder 2 Deactivation/lntake Valve Control Circuit/Open", - "P3410": "Cylinder 2 Deactivation/lntake Valve Control Performance", - "P3411": "Cylinder 2 Deactivation/lntake Valve Control Circuit Low", - "P3412": "Cylinder 2 Deactivation/lntake Valve Control Circuit High", - "P3413": "Cylinder 2 Exhaust Valve Control Circuit/Open", - "P3414": "Cylinder 2 Exhaust Valve Control Performance", - "P3415": "Cylinder 2 Exhaust Valve Control Circuit Low", - "P3416": "Cylinder 2 Exhaust Valve Control Circuit High", - "P3417": "Cylinder 3 Deactivation/lntake Valve Control Circuit/Open", - "P3418": "Cylinder 3 Deactivation/lntake Valve Control Performance", - "P3419": "Cylinder 3 Deactivation/lntake Valve Control Circuit Low", - "P3420": "Cylinder 3 Deactivation/lntake Valve Control Circuit High", - "P3421": "Cylinder 3 Exhaust Valve Control Circuit/Open", - "P3422": "Cylinder 3 Exhaust Valve Control Performance", - "P3423": "Cylinder 3 Exhaust Valve Control Circuit Low", - "P3424": "Cylinder 3 Exhaust Valve Control Circuit High", - "P3425": "Cylinder 4 Deactivation/lntake Valve Control Circuit/Open", - "P3426": "Cylinder 4 Deactivation/lntake Valve Control Performance", - "P3427": "Cylinder 4 Deactivation/lntake Valve Control Circuit Low", - "P3428": "Cylinder 4 Deactivation/lntake Valve Control Circuit High", - "P3429": "Cylinder 4 Exhaust Valve Control Circuit/Open", - "P3430": "Cylinder 4 Exhaust Valve Control Performance", - "P3431": "Cylinder 4 Exhaust Valve Control Circuit Low", - "P3432": "Cylinder 4 Exhaust Valve Control Circuit High", - "P3433": "Cylinder 5 Deactivation/lntake Valve Control Circuit/Open", - "P3434": "Cylinder 5 Deactivation/lntake Valve Control Performance", - "P3435": "Cylinder 5 Deactivation/lntake Valve Control Circuit Low", - "P3436": "Cylinder 5 Deactivation/lntake Valve Control Circuit High", - "P3437": "Cylinder 5 Exhaust Valve Control Circuit/Open", - "P3438": "Cylinder 5 Exhaust Valve Control Performance", - "P3439": "Cylinder 5 Exhaust Valve Control Circuit Low", - "P3440": "Cylinder 5 Exhaust Valve Control Circuit High", - "P3441": "Cylinder 6 Deactivation/lntake Valve Control Circuit/Open", - "P3442": "Cylinder 6 Deactivation/lntake Valve Control Performance", - "P3443": "Cylinder 6 Deactivation/lntake Valve Control Circuit Low", - "P3444": "Cylinder 6 Deactivation/lntake Valve Control Circuit High", - "P3445": "Cylinder 6 Exhaust Valve Control Circuit/Open", - "P3446": "Cylinder 6 Exhaust Valve Control Performance", - "P3447": "Cylinder 6 Exhaust Valve Control Circuit Low", - "P3448": "Cylinder 6 Exhaust Valve Control Circuit High", - "P3449": "Cylinder 7 Deactivation/lntake Valve Control Circuit/Open", - "P3450": "Cylinder 7 Deactivation/lntake Valve Control Performance", - "P3451": "Cylinder 7 Deactivation/lntake Valve Control Circuit Low", - "P3452": "Cylinder 7 Deactivation/lntake Valve Control Circuit High", - "P3453": "Cylinder 7 Exhaust Valve Control Circuit/Open", - "P3454": "Cylinder 7 Exhaust Valve Control Performance", - "P3455": "Cylinder 7 Exhaust Valve Control Circuit Low", - "P3456": "Cylinder 7 Exhaust Valve Control Circuit High", - "P3457": "Cylinder 8 Deactivation/lntake Valve Control Circuit/Open", - "P3458": "Cylinder 8 Deactivation/lntake Valve Control Performance", - "P3459": "Cylinder 8 Deactivation/lntake Valve Control Circuit Low", - "P3460": "Cylinder 8 Deactivation/lntake Valve Control Circuit High", - "P3461": "Cylinder 8 Exhaust Valve Control Circuit/Open", - "P3462": "Cylinder 8 Exhaust Valve Control Performance", - "P3463": "Cylinder 8 Exhaust Valve Control Circuit Low", - "P3464": "Cylinder 8 Exhaust Valve Control Circuit High", - "P3465": "Cylinder 9 Deactivation/lntake Valve Control Circuit/Open", - "P3466": "Cylinder 9 Deactivation/lntake Valve Control Performance", - "P3467": "Cylinder 9 Deactivation/lntake Valve Control Circuit Low", - "P3468": "Cylinder 9 Deactivation/lntake Valve Control Circuit High", - "P3469": "Cylinder 9 Exhaust Valve Control Circuit/Open", - "P3470": "Cylinder 9 Exhaust Valve Control Performance", - "P3471": "Cylinder 9 Exhaust Valve Control Circuit Low", - "P3472": "Cylinder 9 Exhaust Valve Control Circuit High", - "P3473": "Cylinder 10 Deactivation/lntake Valve Control Circuit/Open", - "P3474": "Cylinder 10 Deactivation/lntake Valve Control Performance", - "P3475": "Cylinder 10 Deactivation/lntake Valve Control Circuit Low", - "P3476": "Cylinder 10 Deactivation/lntake Valve Control Circuit High", - "P3477": "Cylinder 10 Exhaust Valve Control Circuit/Open", - "P3478": "Cylinder 10 Exhaust Valve Control Performance", - "P3479": "Cylinder 10 Exhaust Valve Control Circuit Low", - "P3480": "Cylinder 10 Exhaust Valve Control Circuit High", - "P3481": "Cylinder 11 Deactivation/lntake Valve Control Circuit/Open", - "P3482": "Cylinder 11 Deactivation/lntake Valve Control Performance", - "P3483": "Cylinder 11 Deactivation/lntake Valve Control Circuit Low", - "P3484": "Cylinder 11 Deactivation/lntake Valve Control Circuit High", - "P3485": "Cylinder 11 Exhaust Valve Control Circuit/Open", - "P3486": "Cylinder 11 Exhaust Valve Control Performance", - "P3487": "Cylinder 11 Exhaust Valve Control Circuit Low", - "P3488": "Cylinder 11 Exhaust Valve Control Circuit High", - "P3489": "Cylinder 12 Deactivation/lntake Valve Control Circuit/Open", - "P3490": "Cylinder 12 Deactivation/lntake Valve Control Performance", - "P3491": "Cylinder 12 Deactivation/lntake Valve Control Circuit Low", - "P3492": "Cylinder 12 Deactivation/lntake Valve Control Circuit High", - "P3493": "Cylinder 12 Exhaust Valve Control Circuit/Open", - "P3494": "Cylinder 12 Exhaust Valve Control Performance", - "P3495": "Cylinder 12 Exhaust Valve Control Circuit Low", - "P3496": "Cylinder 12 Exhaust Valve Control Circuit High", - "P3497": "Cylinder Deactivation System", + "P0001": "Fuel Volume Regulator Control Circuit/Open", + "P0002": "Fuel Volume Regulator Control Circuit Range/Performance", + "P0003": "Fuel Volume Regulator Control Circuit Low", + "P0004": "Fuel Volume Regulator Control Circuit High", + "P0005": "Fuel Shutoff Valve 'A' Control Circuit/Open", + "P0006": "Fuel Shutoff Valve 'A' Control Circuit Low", + "P0007": "Fuel Shutoff Valve 'A' Control Circuit High", + "P0008": "Engine Position System Performance", + "P0009": "Engine Position System Performance", + "P0010": "'A' Camshaft Position Actuator Circuit", + "P0011": "'A' Camshaft Position - Timing Over-Advanced or System Performance", + "P0012": "'A' Camshaft Position - Timing Over-Retarded", + "P0013": "'B' Camshaft Position - Actuator Circuit", + "P0014": "'B' Camshaft Position - Timing Over-Advanced or System Performance", + "P0015": "'B' Camshaft Position - Timing Over-Retarded", + "P0016": "Crankshaft Position - Camshaft Position Correlation", + "P0017": "Crankshaft Position - Camshaft Position Correlation", + "P0018": "Crankshaft Position - Camshaft Position Correlation", + "P0019": "Crankshaft Position - Camshaft Position Correlation", + "P0020": "'A' Camshaft Position Actuator Circuit", + "P0021": "'A' Camshaft Position - Timing Over-Advanced or System Performance", + "P0022": "'A' Camshaft Position - Timing Over-Retarded", + "P0023": "'B' Camshaft Position - Actuator Circuit", + "P0024": "'B' Camshaft Position - Timing Over-Advanced or System Performance", + "P0025": "'B' Camshaft Position - Timing Over-Retarded", + "P0026": "Intake Valve Control Solenoid Circuit Range/Performance", + "P0027": "Exhaust Valve Control Solenoid Circuit Range/Performance", + "P0028": "Intake Valve Control Solenoid Circuit Range/Performance", + "P0029": "Exhaust Valve Control Solenoid Circuit Range/Performance", + "P0030": "HO2S Heater Control Circuit", + "P0031": "HO2S Heater Control Circuit Low", + "P0032": "HO2S Heater Control Circuit High", + "P0033": "Turbo Charger Bypass Valve Control Circuit", + "P0034": "Turbo Charger Bypass Valve Control Circuit Low", + "P0035": "Turbo Charger Bypass Valve Control Circuit High", + "P0036": "HO2S Heater Control Circuit", + "P0037": "HO2S Heater Control Circuit Low", + "P0038": "HO2S Heater Control Circuit High", + "P0039": "Turbo/Super Charger Bypass Valve Control Circuit Range/Performance", + "P0040": "O2 Sensor Signals Swapped Bank 1 Sensor 1/ Bank 2 Sensor 1", + "P0041": "O2 Sensor Signals Swapped Bank 1 Sensor 2/ Bank 2 Sensor 2", + "P0042": "HO2S Heater Control Circuit", + "P0043": "HO2S Heater Control Circuit Low", + "P0044": "HO2S Heater Control Circuit High", + "P0045": "Turbo/Super Charger Boost Control Solenoid Circuit/Open", + "P0046": "Turbo/Super Charger Boost Control Solenoid Circuit Range/Performance", + "P0047": "Turbo/Super Charger Boost Control Solenoid Circuit Low", + "P0048": "Turbo/Super Charger Boost Control Solenoid Circuit High", + "P0049": "Turbo/Super Charger Turbine Overspeed", + "P0050": "HO2S Heater Control Circuit", + "P0051": "HO2S Heater Control Circuit Low", + "P0052": "HO2S Heater Control Circuit High", + "P0053": "HO2S Heater Resistance", + "P0054": "HO2S Heater Resistance", + "P0055": "HO2S Heater Resistance", + "P0056": "HO2S Heater Control Circuit", + "P0057": "HO2S Heater Control Circuit Low", + "P0058": "HO2S Heater Control Circuit High", + "P0059": "HO2S Heater Resistance", + "P0060": "HO2S Heater Resistance", + "P0061": "HO2S Heater Resistance", + "P0062": "HO2S Heater Control Circuit", + "P0063": "HO2S Heater Control Circuit Low", + "P0064": "HO2S Heater Control Circuit High", + "P0065": "Air Assisted Injector Control Range/Performance", + "P0066": "Air Assisted Injector Control Circuit or Circuit Low", + "P0067": "Air Assisted Injector Control Circuit High", + "P0068": "MAP/MAF - Throttle Position Correlation", + "P0069": "Manifold Absolute Pressure - Barometric Pressure Correlation", + "P0070": "Ambient Air Temperature Sensor Circuit", + "P0071": "Ambient Air Temperature Sensor Range/Performance", + "P0072": "Ambient Air Temperature Sensor Circuit Low", + "P0073": "Ambient Air Temperature Sensor Circuit High", + "P0074": "Ambient Air Temperature Sensor Circuit Intermittent", + "P0075": "Intake Valve Control Solenoid Circuit", + "P0076": "Intake Valve Control Solenoid Circuit Low", + "P0077": "Intake Valve Control Solenoid Circuit High", + "P0078": "Exhaust Valve Control Solenoid Circuit", + "P0079": "Exhaust Valve Control Solenoid Circuit Low", + "P0080": "Exhaust Valve Control Solenoid Circuit High", + "P0081": "Intake Valve Control Solenoid Circuit", + "P0082": "Intake Valve Control Solenoid Circuit Low", + "P0083": "Intake Valve Control Solenoid Circuit High", + "P0084": "Exhaust Valve Control Solenoid Circuit", + "P0085": "Exhaust Valve Control Solenoid Circuit Low", + "P0086": "Exhaust Valve Control Solenoid Circuit High", + "P0087": "Fuel Rail/System Pressure - Too Low", + "P0088": "Fuel Rail/System Pressure - Too High", + "P0089": "Fuel Pressure Regulator 1 Performance", + "P0090": "Fuel Pressure Regulator 1 Control Circuit", + "P0091": "Fuel Pressure Regulator 1 Control Circuit Low", + "P0092": "Fuel Pressure Regulator 1 Control Circuit High", + "P0093": "Fuel System Leak Detected - Large Leak", + "P0094": "Fuel System Leak Detected - Small Leak", + "P0095": "Intake Air Temperature Sensor 2 Circuit", + "P0096": "Intake Air Temperature Sensor 2 Circuit Range/Performance", + "P0097": "Intake Air Temperature Sensor 2 Circuit Low", + "P0098": "Intake Air Temperature Sensor 2 Circuit High", + "P0099": "Intake Air Temperature Sensor 2 Circuit Intermittent/Erratic", + "P0100": "Mass or Volume Air Flow Circuit", + "P0101": "Mass or Volume Air Flow Circuit Range/Performance", + "P0102": "Mass or Volume Air Flow Circuit Low Input", + "P0103": "Mass or Volume Air Flow Circuit High Input", + "P0104": "Mass or Volume Air Flow Circuit Intermittent", + "P0105": "Manifold Absolute Pressure/Barometric Pressure Circuit", + "P0106": "Manifold Absolute Pressure/Barometric Pressure Circuit Range/Performance", + "P0107": "Manifold Absolute Pressure/Barometric Pressure Circuit Low Input", + "P0108": "Manifold Absolute Pressure/Barometric Pressure Circuit High Input", + "P0109": "Manifold Absolute Pressure/Barometric Pressure Circuit Intermittent", + "P0110": "Intake Air Temperature Sensor 1 Circuit", + "P0111": "Intake Air Temperature Sensor 1 Circuit Range/Performance", + "P0112": "Intake Air Temperature Sensor 1 Circuit Low", + "P0113": "Intake Air Temperature Sensor 1 Circuit High", + "P0114": "Intake Air Temperature Sensor 1 Circuit Intermittent", + "P0115": "Engine Coolant Temperature Circuit", + "P0116": "Engine Coolant Temperature Circuit Range/Performance", + "P0117": "Engine Coolant Temperature Circuit Low", + "P0118": "Engine Coolant Temperature Circuit High", + "P0119": "Engine Coolant Temperature Circuit Intermittent", + "P0120": "Throttle/Pedal Position Sensor/Switch 'A' Circuit", + "P0121": "Throttle/Pedal Position Sensor/Switch 'A' Circuit Range/Performance", + "P0122": "Throttle/Pedal Position Sensor/Switch 'A' Circuit Low", + "P0123": "Throttle/Pedal Position Sensor/Switch 'A' Circuit High", + "P0124": "Throttle/Pedal Position Sensor/Switch 'A' Circuit Intermittent", + "P0125": "Insufficient Coolant Temperature for Closed Loop Fuel Control", + "P0126": "Insufficient Coolant Temperature for Stable Operation", + "P0127": "Intake Air Temperature Too High", + "P0128": "Coolant Thermostat (Coolant Temperature Below Thermostat Regulating Temperature)", + "P0129": "Barometric Pressure Too Low", + "P0130": "O2 Sensor Circuit", + "P0131": "O2 Sensor Circuit Low Voltage", + "P0132": "O2 Sensor Circuit High Voltage", + "P0133": "O2 Sensor Circuit Slow Response", + "P0134": "O2 Sensor Circuit No Activity Detected", + "P0135": "O2 Sensor Heater Circuit", + "P0136": "O2 Sensor Circuit", + "P0137": "O2 Sensor Circuit Low Voltage", + "P0138": "O2 Sensor Circuit High Voltage", + "P0139": "O2 Sensor Circuit Slow Response", + "P0140": "O2 Sensor Circuit No Activity Detected", + "P0141": "O2 Sensor Heater Circuit", + "P0142": "O2 Sensor Circuit", + "P0143": "O2 Sensor Circuit Low Voltage", + "P0144": "O2 Sensor Circuit High Voltage", + "P0145": "O2 Sensor Circuit Slow Response", + "P0146": "O2 Sensor Circuit No Activity Detected", + "P0147": "O2 Sensor Heater Circuit", + "P0148": "Fuel Delivery Error", + "P0149": "Fuel Timing Error", + "P0150": "O2 Sensor Circuit", + "P0151": "O2 Sensor Circuit Low Voltage", + "P0152": "O2 Sensor Circuit High Voltage", + "P0153": "O2 Sensor Circuit Slow Response", + "P0154": "O2 Sensor Circuit No Activity Detected", + "P0155": "O2 Sensor Heater Circuit", + "P0156": "O2 Sensor Circuit", + "P0157": "O2 Sensor Circuit Low Voltage", + "P0158": "O2 Sensor Circuit High Voltage", + "P0159": "O2 Sensor Circuit Slow Response", + "P0160": "O2 Sensor Circuit No Activity Detected", + "P0161": "O2 Sensor Heater Circuit", + "P0162": "O2 Sensor Circuit", + "P0163": "O2 Sensor Circuit Low Voltage", + "P0164": "O2 Sensor Circuit High Voltage", + "P0165": "O2 Sensor Circuit Slow Response", + "P0166": "O2 Sensor Circuit No Activity Detected", + "P0167": "O2 Sensor Heater Circuit", + "P0168": "Fuel Temperature Too High", + "P0169": "Incorrect Fuel Composition", + "P0170": "Fuel Trim", + "P0171": "System Too Lean", + "P0172": "System Too Rich", + "P0173": "Fuel Trim", + "P0174": "System Too Lean", + "P0175": "System Too Rich", + "P0176": "Fuel Composition Sensor Circuit", + "P0177": "Fuel Composition Sensor Circuit Range/Performance", + "P0178": "Fuel Composition Sensor Circuit Low", + "P0179": "Fuel Composition Sensor Circuit High", + "P0180": "Fuel Temperature Sensor A Circuit", + "P0181": "Fuel Temperature Sensor A Circuit Range/Performance", + "P0182": "Fuel Temperature Sensor A Circuit Low", + "P0183": "Fuel Temperature Sensor A Circuit High", + "P0184": "Fuel Temperature Sensor A Circuit Intermittent", + "P0185": "Fuel Temperature Sensor B Circuit", + "P0186": "Fuel Temperature Sensor B Circuit Range/Performance", + "P0187": "Fuel Temperature Sensor B Circuit Low", + "P0188": "Fuel Temperature Sensor B Circuit High", + "P0189": "Fuel Temperature Sensor B Circuit Intermittent", + "P0190": "Fuel Rail Pressure Sensor Circuit", + "P0191": "Fuel Rail Pressure Sensor Circuit Range/Performance", + "P0192": "Fuel Rail Pressure Sensor Circuit Low", + "P0193": "Fuel Rail Pressure Sensor Circuit High", + "P0194": "Fuel Rail Pressure Sensor Circuit Intermittent", + "P0195": "Engine Oil Temperature Sensor", + "P0196": "Engine Oil Temperature Sensor Range/Performance", + "P0197": "Engine Oil Temperature Sensor Low", + "P0198": "Engine Oil Temperature Sensor High", + "P0199": "Engine Oil Temperature Sensor Intermittent", + "P0200": "Injector Circuit/Open", + "P0201": "Injector Circuit/Open - Cylinder 1", + "P0202": "Injector Circuit/Open - Cylinder 2", + "P0203": "Injector Circuit/Open - Cylinder 3", + "P0204": "Injector Circuit/Open - Cylinder 4", + "P0205": "Injector Circuit/Open - Cylinder 5", + "P0206": "Injector Circuit/Open - Cylinder 6", + "P0207": "Injector Circuit/Open - Cylinder 7", + "P0208": "Injector Circuit/Open - Cylinder 8", + "P0209": "Injector Circuit/Open - Cylinder 9", + "P0210": "Injector Circuit/Open - Cylinder 10", + "P0211": "Injector Circuit/Open - Cylinder 11", + "P0212": "Injector Circuit/Open - Cylinder 12", + "P0213": "Cold Start Injector 1", + "P0214": "Cold Start Injector 2", + "P0215": "Engine Shutoff Solenoid", + "P0216": "Injector/Injection Timing Control Circuit", + "P0217": "Engine Coolant Over Temperature Condition", + "P0218": "Transmission Fluid Over Temperature Condition", + "P0219": "Engine Overspeed Condition", + "P0220": "Throttle/Pedal Position Sensor/Switch 'B' Circuit", + "P0221": "Throttle/Pedal Position Sensor/Switch 'B' Circuit Range/Performance", + "P0222": "Throttle/Pedal Position Sensor/Switch 'B' Circuit Low", + "P0223": "Throttle/Pedal Position Sensor/Switch 'B' Circuit High", + "P0224": "Throttle/Pedal Position Sensor/Switch 'B' Circuit Intermittent", + "P0225": "Throttle/Pedal Position Sensor/Switch 'C' Circuit", + "P0226": "Throttle/Pedal Position Sensor/Switch 'C' Circuit Range/Performance", + "P0227": "Throttle/Pedal Position Sensor/Switch 'C' Circuit Low", + "P0228": "Throttle/Pedal Position Sensor/Switch 'C' Circuit High", + "P0229": "Throttle/Pedal Position Sensor/Switch 'C' Circuit Intermittent", + "P0230": "Fuel Pump Primary Circuit", + "P0231": "Fuel Pump Secondary Circuit Low", + "P0232": "Fuel Pump Secondary Circuit High", + "P0233": "Fuel Pump Secondary Circuit Intermittent", + "P0234": "Turbo/Super Charger Overboost Condition", + "P0235": "Turbo/Super Charger Boost Sensor 'A' Circuit", + "P0236": "Turbo/Super Charger Boost Sensor 'A' Circuit Range/Performance", + "P0237": "Turbo/Super Charger Boost Sensor 'A' Circuit Low", + "P0238": "Turbo/Super Charger Boost Sensor 'A' Circuit High", + "P0239": "Turbo/Super Charger Boost Sensor 'B' Circuit", + "P0240": "Turbo/Super Charger Boost Sensor 'B' Circuit Range/Performance", + "P0241": "Turbo/Super Charger Boost Sensor 'B' Circuit Low", + "P0242": "Turbo/Super Charger Boost Sensor 'B' Circuit High", + "P0243": "Turbo/Super Charger Wastegate Solenoid 'A'", + "P0244": "Turbo/Super Charger Wastegate Solenoid 'A' Range/Performance", + "P0245": "Turbo/Super Charger Wastegate Solenoid 'A' Low", + "P0246": "Turbo/Super Charger Wastegate Solenoid 'A' High", + "P0247": "Turbo/Super Charger Wastegate Solenoid 'B'", + "P0248": "Turbo/Super Charger Wastegate Solenoid 'B' Range/Performance", + "P0249": "Turbo/Super Charger Wastegate Solenoid 'B' Low", + "P0250": "Turbo/Super Charger Wastegate Solenoid 'B' High", + "P0251": "Injection Pump Fuel Metering Control 'A' (Cam/Rotor/Injector)", + "P0252": "Injection Pump Fuel Metering Control 'A' Range/Performance (Cam/Rotor/Injector)", + "P0253": "Injection Pump Fuel Metering Control 'A' Low (Cam/Rotor/Injector)", + "P0254": "Injection Pump Fuel Metering Control 'A' High (Cam/Rotor/Injector)", + "P0255": "Injection Pump Fuel Metering Control 'A' Intermittent (Cam/Rotor/Injector)", + "P0256": "Injection Pump Fuel Metering Control 'B' (Cam/Rotor/Injector)", + "P0257": "Injection Pump Fuel Metering Control 'B' Range/Performance (Cam/Rotor/Injector)", + "P0258": "Injection Pump Fuel Metering Control 'B' Low (Cam/Rotor/Injector)", + "P0259": "Injection Pump Fuel Metering Control 'B' High (Cam/Rotor/Injector)", + "P0260": "Injection Pump Fuel Metering Control 'B' Intermittent (Cam/Rotor/Injector)", + "P0261": "Cylinder 1 Injector Circuit Low", + "P0262": "Cylinder 1 Injector Circuit High", + "P0263": "Cylinder 1 Contribution/Balance", + "P0264": "Cylinder 2 Injector Circuit Low", + "P0265": "Cylinder 2 Injector Circuit High", + "P0266": "Cylinder 2 Contribution/Balance", + "P0267": "Cylinder 3 Injector Circuit Low", + "P0268": "Cylinder 3 Injector Circuit High", + "P0269": "Cylinder 3 Contribution/Balance", + "P0270": "Cylinder 4 Injector Circuit Low", + "P0271": "Cylinder 4 Injector Circuit High", + "P0272": "Cylinder 4 Contribution/Balance", + "P0273": "Cylinder 5 Injector Circuit Low", + "P0274": "Cylinder 5 Injector Circuit High", + "P0275": "Cylinder 5 Contribution/Balance", + "P0276": "Cylinder 6 Injector Circuit Low", + "P0277": "Cylinder 6 Injector Circuit High", + "P0278": "Cylinder 6 Contribution/Balance", + "P0279": "Cylinder 7 Injector Circuit Low", + "P0280": "Cylinder 7 Injector Circuit High", + "P0281": "Cylinder 7 Contribution/Balance", + "P0282": "Cylinder 8 Injector Circuit Low", + "P0283": "Cylinder 8 Injector Circuit High", + "P0284": "Cylinder 8 Contribution/Balance", + "P0285": "Cylinder 9 Injector Circuit Low", + "P0286": "Cylinder 9 Injector Circuit High", + "P0287": "Cylinder 9 Contribution/Balance", + "P0288": "Cylinder 10 Injector Circuit Low", + "P0289": "Cylinder 10 Injector Circuit High", + "P0290": "Cylinder 10 Contribution/Balance", + "P0291": "Cylinder 11 Injector Circuit Low", + "P0292": "Cylinder 11 Injector Circuit High", + "P0293": "Cylinder 11 Contribution/Balance", + "P0294": "Cylinder 12 Injector Circuit Low", + "P0295": "Cylinder 12 Injector Circuit High", + "P0296": "Cylinder 12 Contribution/Balance", + "P0297": "Vehicle Overspeed Condition", + "P0298": "Engine Oil Over Temperature", + "P0299": "Turbo/Super Charger Underboost", + "P0300": "Random/Multiple Cylinder Misfire Detected", + "P0301": "Cylinder 1 Misfire Detected", + "P0302": "Cylinder 2 Misfire Detected", + "P0303": "Cylinder 3 Misfire Detected", + "P0304": "Cylinder 4 Misfire Detected", + "P0305": "Cylinder 5 Misfire Detected", + "P0306": "Cylinder 6 Misfire Detected", + "P0307": "Cylinder 7 Misfire Detected", + "P0308": "Cylinder 8 Misfire Detected", + "P0309": "Cylinder 9 Misfire Detected", + "P0310": "Cylinder 10 Misfire Detected", + "P0311": "Cylinder 11 Misfire Detected", + "P0312": "Cylinder 12 Misfire Detected", + "P0313": "Misfire Detected with Low Fuel", + "P0314": "Single Cylinder Misfire (Cylinder not Specified)", + "P0315": "Crankshaft Position System Variation Not Learned", + "P0316": "Engine Misfire Detected on Startup (First 1000 Revolutions)", + "P0317": "Rough Road Hardware Not Present", + "P0318": "Rough Road Sensor 'A' Signal Circuit", + "P0319": "Rough Road Sensor 'B'", + "P0320": "Ignition/Distributor Engine Speed Input Circuit", + "P0321": "Ignition/Distributor Engine Speed Input Circuit Range/Performance", + "P0322": "Ignition/Distributor Engine Speed Input Circuit No Signal", + "P0323": "Ignition/Distributor Engine Speed Input Circuit Intermittent", + "P0324": "Knock Control System Error", + "P0325": "Knock Sensor 1 Circuit", + "P0326": "Knock Sensor 1 Circuit Range/Performance", + "P0327": "Knock Sensor 1 Circuit Low", + "P0328": "Knock Sensor 1 Circuit High", + "P0329": "Knock Sensor 1 Circuit Input Intermittent", + "P0330": "Knock Sensor 2 Circuit", + "P0331": "Knock Sensor 2 Circuit Range/Performance", + "P0332": "Knock Sensor 2 Circuit Low", + "P0333": "Knock Sensor 2 Circuit High", + "P0334": "Knock Sensor 2 Circuit Input Intermittent", + "P0335": "Crankshaft Position Sensor 'A' Circuit", + "P0336": "Crankshaft Position Sensor 'A' Circuit Range/Performance", + "P0337": "Crankshaft Position Sensor 'A' Circuit Low", + "P0338": "Crankshaft Position Sensor 'A' Circuit High", + "P0339": "Crankshaft Position Sensor 'A' Circuit Intermittent", + "P0340": "Camshaft Position Sensor 'A' Circuit", + "P0341": "Camshaft Position Sensor 'A' Circuit Range/Performance", + "P0342": "Camshaft Position Sensor 'A' Circuit Low", + "P0343": "Camshaft Position Sensor 'A' Circuit High", + "P0344": "Camshaft Position Sensor 'A' Circuit Intermittent", + "P0345": "Camshaft Position Sensor 'A' Circuit", + "P0346": "Camshaft Position Sensor 'A' Circuit Range/Performance", + "P0347": "Camshaft Position Sensor 'A' Circuit Low", + "P0348": "Camshaft Position Sensor 'A' Circuit High", + "P0349": "Camshaft Position Sensor 'A' Circuit Intermittent", + "P0350": "Ignition Coil Primary/Secondary Circuit", + "P0351": "Ignition Coil 'A' Primary/Secondary Circuit", + "P0352": "Ignition Coil 'B' Primary/Secondary Circuit", + "P0353": "Ignition Coil 'C' Primary/Secondary Circuit", + "P0354": "Ignition Coil 'D' Primary/Secondary Circuit", + "P0355": "Ignition Coil 'E' Primary/Secondary Circuit", + "P0356": "Ignition Coil 'F' Primary/Secondary Circuit", + "P0357": "Ignition Coil 'G' Primary/Secondary Circuit", + "P0358": "Ignition Coil 'H' Primary/Secondary Circuit", + "P0359": "Ignition Coil 'I' Primary/Secondary Circuit", + "P0360": "Ignition Coil 'J' Primary/Secondary Circuit", + "P0361": "Ignition Coil 'K' Primary/Secondary Circuit", + "P0362": "Ignition Coil 'L' Primary/Secondary Circuit", + "P0363": "Misfire Detected - Fueling Disabled", + "P0364": "Reserved", + "P0365": "Camshaft Position Sensor 'B' Circuit", + "P0366": "Camshaft Position Sensor 'B' Circuit Range/Performance", + "P0367": "Camshaft Position Sensor 'B' Circuit Low", + "P0368": "Camshaft Position Sensor 'B' Circuit High", + "P0369": "Camshaft Position Sensor 'B' Circuit Intermittent", + "P0370": "Timing Reference High Resolution Signal 'A'", + "P0371": "Timing Reference High Resolution Signal 'A' Too Many Pulses", + "P0372": "Timing Reference High Resolution Signal 'A' Too Few Pulses", + "P0373": "Timing Reference High Resolution Signal 'A' Intermittent/Erratic Pulses", + "P0374": "Timing Reference High Resolution Signal 'A' No Pulse", + "P0375": "Timing Reference High Resolution Signal 'B'", + "P0376": "Timing Reference High Resolution Signal 'B' Too Many Pulses", + "P0377": "Timing Reference High Resolution Signal 'B' Too Few Pulses", + "P0378": "Timing Reference High Resolution Signal 'B' Intermittent/Erratic Pulses", + "P0379": "Timing Reference High Resolution Signal 'B' No Pulses", + "P0380": "Glow Plug/Heater Circuit 'A'", + "P0381": "Glow Plug/Heater Indicator Circuit", + "P0382": "Glow Plug/Heater Circuit 'B'", + "P0383": "Reserved by SAE J2012", + "P0384": "Reserved by SAE J2012", + "P0385": "Crankshaft Position Sensor 'B' Circuit", + "P0386": "Crankshaft Position Sensor 'B' Circuit Range/Performance", + "P0387": "Crankshaft Position Sensor 'B' Circuit Low", + "P0388": "Crankshaft Position Sensor 'B' Circuit High", + "P0389": "Crankshaft Position Sensor 'B' Circuit Intermittent", + "P0390": "Camshaft Position Sensor 'B' Circuit", + "P0391": "Camshaft Position Sensor 'B' Circuit Range/Performance", + "P0392": "Camshaft Position Sensor 'B' Circuit Low", + "P0393": "Camshaft Position Sensor 'B' Circuit High", + "P0394": "Camshaft Position Sensor 'B' Circuit Intermittent", + "P0400": "Exhaust Gas Recirculation Flow", + "P0401": "Exhaust Gas Recirculation Flow Insufficient Detected", + "P0402": "Exhaust Gas Recirculation Flow Excessive Detected", + "P0403": "Exhaust Gas Recirculation Control Circuit", + "P0404": "Exhaust Gas Recirculation Control Circuit Range/Performance", + "P0405": "Exhaust Gas Recirculation Sensor 'A' Circuit Low", + "P0406": "Exhaust Gas Recirculation Sensor 'A' Circuit High", + "P0407": "Exhaust Gas Recirculation Sensor 'B' Circuit Low", + "P0408": "Exhaust Gas Recirculation Sensor 'B' Circuit High", + "P0409": "Exhaust Gas Recirculation Sensor 'A' Circuit", + "P0410": "Secondary Air Injection System", + "P0411": "Secondary Air Injection System Incorrect Flow Detected", + "P0412": "Secondary Air Injection System Switching Valve 'A' Circuit", + "P0413": "Secondary Air Injection System Switching Valve 'A' Circuit Open", + "P0414": "Secondary Air Injection System Switching Valve 'A' Circuit Shorted", + "P0415": "Secondary Air Injection System Switching Valve 'B' Circuit", + "P0416": "Secondary Air Injection System Switching Valve 'B' Circuit Open", + "P0417": "Secondary Air Injection System Switching Valve 'B' Circuit Shorted", + "P0418": "Secondary Air Injection System Control 'A' Circuit", + "P0419": "Secondary Air Injection System Control 'B' Circuit", + "P0420": "Catalyst System Efficiency Below Threshold", + "P0421": "Warm Up Catalyst Efficiency Below Threshold", + "P0422": "Main Catalyst Efficiency Below Threshold", + "P0423": "Heated Catalyst Efficiency Below Threshold", + "P0424": "Heated Catalyst Temperature Below Threshold", + "P0425": "Catalyst Temperature Sensor", + "P0426": "Catalyst Temperature Sensor Range/Performance", + "P0427": "Catalyst Temperature Sensor Low", + "P0428": "Catalyst Temperature Sensor High", + "P0429": "Catalyst Heater Control Circuit", + "P0430": "Catalyst System Efficiency Below Threshold", + "P0431": "Warm Up Catalyst Efficiency Below Threshold", + "P0432": "Main Catalyst Efficiency Below Threshold", + "P0433": "Heated Catalyst Efficiency Below Threshold", + "P0434": "Heated Catalyst Temperature Below Threshold", + "P0435": "Catalyst Temperature Sensor", + "P0436": "Catalyst Temperature Sensor Range/Performance", + "P0437": "Catalyst Temperature Sensor Low", + "P0438": "Catalyst Temperature Sensor High", + "P0439": "Catalyst Heater Control Circuit", + "P0440": "Evaporative Emission System", + "P0441": "Evaporative Emission System Incorrect Purge Flow", + "P0442": "Evaporative Emission System Leak Detected (small leak)", + "P0443": "Evaporative Emission System Purge Control Valve Circuit", + "P0444": "Evaporative Emission System Purge Control Valve Circuit Open", + "P0445": "Evaporative Emission System Purge Control Valve Circuit Shorted", + "P0446": "Evaporative Emission System Vent Control Circuit", + "P0447": "Evaporative Emission System Vent Control Circuit Open", + "P0448": "Evaporative Emission System Vent Control Circuit Shorted", + "P0449": "Evaporative Emission System Vent Valve/Solenoid Circuit", + "P0450": "Evaporative Emission System Pressure Sensor/Switch", + "P0451": "Evaporative Emission System Pressure Sensor/Switch Range/Performance", + "P0452": "Evaporative Emission System Pressure Sensor/Switch Low", + "P0453": "Evaporative Emission System Pressure Sensor/Switch High", + "P0454": "Evaporative Emission System Pressure Sensor/Switch Intermittent", + "P0455": "Evaporative Emission System Leak Detected (large leak)", + "P0456": "Evaporative Emission System Leak Detected (very small leak)", + "P0457": "Evaporative Emission System Leak Detected (fuel cap loose/off)", + "P0458": "Evaporative Emission System Purge Control Valve Circuit Low", + "P0459": "Evaporative Emission System Purge Control Valve Circuit High", + "P0460": "Fuel Level Sensor 'A' Circuit", + "P0461": "Fuel Level Sensor 'A' Circuit Range/Performance", + "P0462": "Fuel Level Sensor 'A' Circuit Low", + "P0463": "Fuel Level Sensor 'A' Circuit High", + "P0464": "Fuel Level Sensor 'A' Circuit Intermittent", + "P0465": "EVAP Purge Flow Sensor Circuit", + "P0466": "EVAP Purge Flow Sensor Circuit Range/Performance", + "P0467": "EVAP Purge Flow Sensor Circuit Low", + "P0468": "EVAP Purge Flow Sensor Circuit High", + "P0469": "EVAP Purge Flow Sensor Circuit Intermittent", + "P0470": "Exhaust Pressure Sensor", + "P0471": "Exhaust Pressure Sensor Range/Performance", + "P0472": "Exhaust Pressure Sensor Low", + "P0473": "Exhaust Pressure Sensor High", + "P0474": "Exhaust Pressure Sensor Intermittent", + "P0475": "Exhaust Pressure Control Valve", + "P0476": "Exhaust Pressure Control Valve Range/Performance", + "P0477": "Exhaust Pressure Control Valve Low", + "P0478": "Exhaust Pressure Control Valve High", + "P0479": "Exhaust Pressure Control Valve Intermittent", + "P0480": "Fan 1 Control Circuit", + "P0481": "Fan 2 Control Circuit", + "P0482": "Fan 3 Control Circuit", + "P0483": "Fan Rationality Check", + "P0484": "Fan Circuit Over Current", + "P0485": "Fan Power/Ground Circuit", + "P0486": "Exhaust Gas Recirculation Sensor 'B' Circuit", + "P0487": "Exhaust Gas Recirculation Throttle Position Control Circuit", + "P0488": "Exhaust Gas Recirculation Throttle Position Control Range/Performance", + "P0489": "Exhaust Gas Recirculation Control Circuit Low", + "P0490": "Exhaust Gas Recirculation Control Circuit High", + "P0491": "Secondary Air Injection System Insufficient Flow", + "P0492": "Secondary Air Injection System Insufficient Flow", + "P0493": "Fan Overspeed", + "P0494": "Fan Speed Low", + "P0495": "Fan Speed High", + "P0496": "Evaporative Emission System High Purge Flow", + "P0497": "Evaporative Emission System Low Purge Flow", + "P0498": "Evaporative Emission System Vent Valve Control Circuit Low", + "P0499": "Evaporative Emission System Vent Valve Control Circuit High", + "P0500": "Vehicle Speed Sensor 'A'", + "P0501": "Vehicle Speed Sensor 'A' Range/Performance", + "P0502": "Vehicle Speed Sensor 'A' Circuit Low Input", + "P0503": "Vehicle Speed Sensor 'A' Intermittent/Erratic/High", + "P0504": "Brake Switch 'A'/'B' Correlation", + "P0505": "Idle Air Control System", + "P0506": "Idle Air Control System RPM Lower Than Expected", + "P0507": "Idle Air Control System RPM Higher Than Expected", + "P0508": "Idle Air Control System Circuit Low", + "P0509": "Idle Air Control System Circuit High", + "P0510": "Closed Throttle Position Switch", + "P0511": "Idle Air Control Circuit", + "P0512": "Starter Request Circuit", + "P0513": "Incorrect Immobilizer Key", + "P0514": "Battery Temperature Sensor Circuit Range/Performance", + "P0515": "Battery Temperature Sensor Circuit", + "P0516": "Battery Temperature Sensor Circuit Low", + "P0517": "Battery Temperature Sensor Circuit High", + "P0518": "Idle Air Control Circuit Intermittent", + "P0519": "Idle Air Control System Performance", + "P0520": "Engine Oil Pressure Sensor/Switch Circuit", + "P0521": "Engine Oil Pressure Sensor/Switch Range/Performance", + "P0522": "Engine Oil Pressure Sensor/Switch Low Voltage", + "P0523": "Engine Oil Pressure Sensor/Switch High Voltage", + "P0524": "Engine Oil Pressure Too Low", + "P0525": "Cruise Control Servo Control Circuit Range/Performance", + "P0526": "Fan Speed Sensor Circuit", + "P0527": "Fan Speed Sensor Circuit Range/Performance", + "P0528": "Fan Speed Sensor Circuit No Signal", + "P0529": "Fan Speed Sensor Circuit Intermittent", + "P0530": "A/C Refrigerant Pressure Sensor 'A' Circuit", + "P0531": "A/C Refrigerant Pressure Sensor 'A' Circuit Range/Performance", + "P0532": "A/C Refrigerant Pressure Sensor 'A' Circuit Low", + "P0533": "A/C Refrigerant Pressure Sensor 'A' Circuit High", + "P0534": "Air Conditioner Refrigerant Charge Loss", + "P0535": "A/C Evaporator Temperature Sensor Circuit", + "P0536": "A/C Evaporator Temperature Sensor Circuit Range/Performance", + "P0537": "A/C Evaporator Temperature Sensor Circuit Low", + "P0538": "A/C Evaporator Temperature Sensor Circuit High", + "P0539": "A/C Evaporator Temperature Sensor Circuit Intermittent", + "P0540": "Intake Air Heater 'A' Circuit", + "P0541": "Intake Air Heater 'A' Circuit Low", + "P0542": "Intake Air Heater 'A' Circuit High", + "P0543": "Intake Air Heater 'A' Circuit Open", + "P0544": "Exhaust Gas Temperature Sensor Circuit", + "P0545": "Exhaust Gas Temperature Sensor Circuit Low", + "P0546": "Exhaust Gas Temperature Sensor Circuit High", + "P0547": "Exhaust Gas Temperature Sensor Circuit", + "P0548": "Exhaust Gas Temperature Sensor Circuit Low", + "P0549": "Exhaust Gas Temperature Sensor Circuit High", + "P0550": "Power Steering Pressure Sensor/Switch Circuit", + "P0551": "Power Steering Pressure Sensor/Switch Circuit Range/Performance", + "P0552": "Power Steering Pressure Sensor/Switch Circuit Low Input", + "P0553": "Power Steering Pressure Sensor/Switch Circuit High Input", + "P0554": "Power Steering Pressure Sensor/Switch Circuit Intermittent", + "P0555": "Brake Booster Pressure Sensor Circuit", + "P0556": "Brake Booster Pressure Sensor Circuit Range/Performance", + "P0557": "Brake Booster Pressure Sensor Circuit Low Input", + "P0558": "Brake Booster Pressure Sensor Circuit High Input", + "P0559": "Brake Booster Pressure Sensor Circuit Intermittent", + "P0560": "System Voltage", + "P0561": "System Voltage Unstable", + "P0562": "System Voltage Low", + "P0563": "System Voltage High", + "P0564": "Cruise Control Multi-Function Input 'A' Circuit", + "P0565": "Cruise Control On Signal", + "P0566": "Cruise Control Off Signal", + "P0567": "Cruise Control Resume Signal", + "P0568": "Cruise Control Set Signal", + "P0569": "Cruise Control Coast Signal", + "P0570": "Cruise Control Accelerate Signal", + "P0571": "Brake Switch 'A' Circuit", + "P0572": "Brake Switch 'A' Circuit Low", + "P0573": "Brake Switch 'A' Circuit High", + "P0574": "Cruise Control System - Vehicle Speed Too High", + "P0575": "Cruise Control Input Circuit", + "P0576": "Cruise Control Input Circuit Low", + "P0577": "Cruise Control Input Circuit High", + "P0578": "Cruise Control Multi-Function Input 'A' Circuit Stuck", + "P0579": "Cruise Control Multi-Function Input 'A' Circuit Range/Performance", + "P0580": "Cruise Control Multi-Function Input 'A' Circuit Low", + "P0581": "Cruise Control Multi-Function Input 'A' Circuit High", + "P0582": "Cruise Control Vacuum Control Circuit/Open", + "P0583": "Cruise Control Vacuum Control Circuit Low", + "P0584": "Cruise Control Vacuum Control Circuit High", + "P0585": "Cruise Control Multi-Function Input 'A'/'B' Correlation", + "P0586": "Cruise Control Vent Control Circuit/Open", + "P0587": "Cruise Control Vent Control Circuit Low", + "P0588": "Cruise Control Vent Control Circuit High", + "P0589": "Cruise Control Multi-Function Input 'B' Circuit", + "P0590": "Cruise Control Multi-Function Input 'B' Circuit Stuck", + "P0591": "Cruise Control Multi-Function Input 'B' Circuit Range/Performance", + "P0592": "Cruise Control Multi-Function Input 'B' Circuit Low", + "P0593": "Cruise Control Multi-Function Input 'B' Circuit High", + "P0594": "Cruise Control Servo Control Circuit/Open", + "P0595": "Cruise Control Servo Control Circuit Low", + "P0596": "Cruise Control Servo Control Circuit High", + "P0597": "Thermostat Heater Control Circuit/Open", + "P0598": "Thermostat Heater Control Circuit Low", + "P0599": "Thermostat Heater Control Circuit High", + "P0600": "Serial Communication Link", + "P0601": "Internal Control Module Memory Check Sum Error", + "P0602": "Control Module Programming Error", + "P0603": "Internal Control Module Keep Alive Memory (KAM) Error", + "P0604": "Internal Control Module Random Access Memory (RAM) Error", + "P0605": "Internal Control Module Read Only Memory (ROM) Error", + "P0606": "ECM/PCM Processor", + "P0607": "Control Module Performance", + "P0608": "Control Module VSS Output 'A'", + "P0609": "Control Module VSS Output 'B'", + "P0610": "Control Module Vehicle Options Error", + "P0611": "Fuel Injector Control Module Performance", + "P0612": "Fuel Injector Control Module Relay Control", + "P0613": "TCM Processor", + "P0614": "ECM / TCM Incompatible", + "P0615": "Starter Relay Circuit", + "P0616": "Starter Relay Circuit Low", + "P0617": "Starter Relay Circuit High", + "P0618": "Alternative Fuel Control Module KAM Error", + "P0619": "Alternative Fuel Control Module RAM/ROM Error", + "P0620": "Generator Control Circuit", + "P0621": "Generator Lamp/L Terminal Circuit", + "P0622": "Generator Field/F Terminal Circuit", + "P0623": "Generator Lamp Control Circuit", + "P0624": "Fuel Cap Lamp Control Circuit", + "P0625": "Generator Field/F Terminal Circuit Low", + "P0626": "Generator Field/F Terminal Circuit High", + "P0627": "Fuel Pump 'A' Control Circuit /Open", + "P0628": "Fuel Pump 'A' Control Circuit Low", + "P0629": "Fuel Pump 'A' Control Circuit High", + "P0630": "VIN Not Programmed or Incompatible - ECM/PCM", + "P0631": "VIN Not Programmed or Incompatible - TCM", + "P0632": "Odometer Not Programmed - ECM/PCM", + "P0633": "Immobilizer Key Not Programmed - ECM/PCM", + "P0634": "PCM/ECM/TCM Internal Temperature Too High", + "P0635": "Power Steering Control Circuit", + "P0636": "Power Steering Control Circuit Low", + "P0637": "Power Steering Control Circuit High", + "P0638": "Throttle Actuator Control Range/Performance", + "P0639": "Throttle Actuator Control Range/Performance", + "P0640": "Intake Air Heater Control Circuit", + "P0641": "Sensor Reference Voltage 'A' Circuit/Open", + "P0642": "Sensor Reference Voltage 'A' Circuit Low", + "P0643": "Sensor Reference Voltage 'A' Circuit High", + "P0644": "Driver Display Serial Communication Circuit", + "P0645": "A/C Clutch Relay Control Circuit", + "P0646": "A/C Clutch Relay Control Circuit Low", + "P0647": "A/C Clutch Relay Control Circuit High", + "P0648": "Immobilizer Lamp Control Circuit", + "P0649": "Speed Control Lamp Control Circuit", + "P0650": "Malfunction Indicator Lamp (MIL) Control Circuit", + "P0651": "Sensor Reference Voltage 'B' Circuit/Open", + "P0652": "Sensor Reference Voltage 'B' Circuit Low", + "P0653": "Sensor Reference Voltage 'B' Circuit High", + "P0654": "Engine RPM Output Circuit", + "P0655": "Engine Hot Lamp Output Control Circuit", + "P0656": "Fuel Level Output Circuit", + "P0657": "Actuator Supply Voltage 'A' Circuit/Open", + "P0658": "Actuator Supply Voltage 'A' Circuit Low", + "P0659": "Actuator Supply Voltage 'A' Circuit High", + "P0660": "Intake Manifold Tuning Valve Control Circuit/Open", + "P0661": "Intake Manifold Tuning Valve Control Circuit Low", + "P0662": "Intake Manifold Tuning Valve Control Circuit High", + "P0663": "Intake Manifold Tuning Valve Control Circuit/Open", + "P0664": "Intake Manifold Tuning Valve Control Circuit Low", + "P0665": "Intake Manifold Tuning Valve Control Circuit High", + "P0666": "PCM/ECM/TCM Internal Temperature Sensor Circuit", + "P0667": "PCM/ECM/TCM Internal Temperature Sensor Range/Performance", + "P0668": "PCM/ECM/TCM Internal Temperature Sensor Circuit Low", + "P0669": "PCM/ECM/TCM Internal Temperature Sensor Circuit High", + "P0670": "Glow Plug Module Control Circuit", + "P0671": "Cylinder 1 Glow Plug Circuit", + "P0672": "Cylinder 2 Glow Plug Circuit", + "P0673": "Cylinder 3 Glow Plug Circuit", + "P0674": "Cylinder 4 Glow Plug Circuit", + "P0675": "Cylinder 5 Glow Plug Circuit", + "P0676": "Cylinder 6 Glow Plug Circuit", + "P0677": "Cylinder 7 Glow Plug Circuit", + "P0678": "Cylinder 8 Glow Plug Circuit", + "P0679": "Cylinder 9 Glow Plug Circuit", + "P0680": "Cylinder 10 Glow Plug Circuit", + "P0681": "Cylinder 11 Glow Plug Circuit", + "P0682": "Cylinder 12 Glow Plug Circuit", + "P0683": "Glow Plug Control Module to PCM Communication Circuit", + "P0684": "Glow Plug Control Module to PCM Communication Circuit Range/Performance", + "P0685": "ECM/PCM Power Relay Control Circuit /Open", + "P0686": "ECM/PCM Power Relay Control Circuit Low", + "P0687": "ECM/PCM Power Relay Control Circuit High", + "P0688": "ECM/PCM Power Relay Sense Circuit /Open", + "P0689": "ECM/PCM Power Relay Sense Circuit Low", + "P0690": "ECM/PCM Power Relay Sense Circuit High", + "P0691": "Fan 1 Control Circuit Low", + "P0692": "Fan 1 Control Circuit High", + "P0693": "Fan 2 Control Circuit Low", + "P0694": "Fan 2 Control Circuit High", + "P0695": "Fan 3 Control Circuit Low", + "P0696": "Fan 3 Control Circuit High", + "P0697": "Sensor Reference Voltage 'C' Circuit/Open", + "P0698": "Sensor Reference Voltage 'C' Circuit Low", + "P0699": "Sensor Reference Voltage 'C' Circuit High", + "P0700": "Transmission Control System (MIL Request)", + "P0701": "Transmission Control System Range/Performance", + "P0702": "Transmission Control System Electrical", + "P0703": "Brake Switch 'B' Circuit", + "P0704": "Clutch Switch Input Circuit Malfunction", + "P0705": "Transmission Range Sensor Circuit Malfunction (PRNDL Input)", + "P0706": "Transmission Range Sensor Circuit Range/Performance", + "P0707": "Transmission Range Sensor Circuit Low", + "P0708": "Transmission Range Sensor Circuit High", + "P0709": "Transmission Range Sensor Circuit Intermittent", + "P0710": "Transmission Fluid Temperature Sensor 'A' Circuit", + "P0711": "Transmission Fluid Temperature Sensor 'A' Circuit Range/Performance", + "P0712": "Transmission Fluid Temperature Sensor 'A' Circuit Low", + "P0713": "Transmission Fluid Temperature Sensor 'A' Circuit High", + "P0714": "Transmission Fluid Temperature Sensor 'A' Circuit Intermittent", + "P0715": "Input/Turbine Speed Sensor 'A' Circuit", + "P0716": "Input/Turbine Speed Sensor 'A' Circuit Range/Performance", + "P0717": "Input/Turbine Speed Sensor 'A' Circuit No Signal", + "P0718": "Input/Turbine Speed Sensor 'A' Circuit Intermittent", + "P0719": "Brake Switch 'B' Circuit Low", + "P0720": "Output Speed Sensor Circuit", + "P0721": "Output Speed Sensor Circuit Range/Performance", + "P0722": "Output Speed Sensor Circuit No Signal", + "P0723": "Output Speed Sensor Circuit Intermittent", + "P0724": "Brake Switch 'B' Circuit High", + "P0725": "Engine Speed Input Circuit", + "P0726": "Engine Speed Input Circuit Range/Performance", + "P0727": "Engine Speed Input Circuit No Signal", + "P0728": "Engine Speed Input Circuit Intermittent", + "P0729": "Gear 6 Incorrect Ratio", + "P0730": "Incorrect Gear Ratio", + "P0731": "Gear 1 Incorrect Ratio", + "P0732": "Gear 2 Incorrect Ratio", + "P0733": "Gear 3 Incorrect Ratio", + "P0734": "Gear 4 Incorrect Ratio", + "P0735": "Gear 5 Incorrect Ratio", + "P0736": "Reverse Incorrect Ratio", + "P0737": "TCM Engine Speed Output Circuit", + "P0738": "TCM Engine Speed Output Circuit Low", + "P0739": "TCM Engine Speed Output Circuit High", + "P0740": "Torque Converter Clutch Circuit/Open", + "P0741": "Torque Converter Clutch Circuit Performance or Stuck Off", + "P0742": "Torque Converter Clutch Circuit Stuck On", + "P0743": "Torque Converter Clutch Circuit Electrical", + "P0744": "Torque Converter Clutch Circuit Intermittent", + "P0745": "Pressure Control Solenoid 'A'", + "P0746": "Pressure Control Solenoid 'A' Performance or Stuck Off", + "P0747": "Pressure Control Solenoid 'A' Stuck On", + "P0748": "Pressure Control Solenoid 'A' Electrical", + "P0749": "Pressure Control Solenoid 'A' Intermittent", + "P0750": "Shift Solenoid 'A'", + "P0751": "Shift Solenoid 'A' Performance or Stuck Off", + "P0752": "Shift Solenoid 'A' Stuck On", + "P0753": "Shift Solenoid 'A' Electrical", + "P0754": "Shift Solenoid 'A' Intermittent", + "P0755": "Shift Solenoid 'B'", + "P0756": "Shift Solenoid 'B' Performance or Stuck Off", + "P0757": "Shift Solenoid 'B' Stuck On", + "P0758": "Shift Solenoid 'B' Electrical", + "P0759": "Shift Solenoid 'B' Intermittent", + "P0760": "Shift Solenoid 'C'", + "P0761": "Shift Solenoid 'C' Performance or Stuck Off", + "P0762": "Shift Solenoid 'C' Stuck On", + "P0763": "Shift Solenoid 'C' Electrical", + "P0764": "Shift Solenoid 'C' Intermittent", + "P0765": "Shift Solenoid 'D'", + "P0766": "Shift Solenoid 'D' Performance or Stuck Off", + "P0767": "Shift Solenoid 'D' Stuck On", + "P0768": "Shift Solenoid 'D' Electrical", + "P0769": "Shift Solenoid 'D' Intermittent", + "P0770": "Shift Solenoid 'E'", + "P0771": "Shift Solenoid 'E' Performance or Stuck Off", + "P0772": "Shift Solenoid 'E' Stuck On", + "P0773": "Shift Solenoid 'E' Electrical", + "P0774": "Shift Solenoid 'E' Intermittent", + "P0775": "Pressure Control Solenoid 'B'", + "P0776": "Pressure Control Solenoid 'B' Performance or Stuck off", + "P0777": "Pressure Control Solenoid 'B' Stuck On", + "P0778": "Pressure Control Solenoid 'B' Electrical", + "P0779": "Pressure Control Solenoid 'B' Intermittent", + "P0780": "Shift Error", + "P0781": "1-2 Shift", + "P0782": "2-3 Shift", + "P0783": "3-4 Shift", + "P0784": "4-5 Shift", + "P0785": "Shift/Timing Solenoid", + "P0786": "Shift/Timing Solenoid Range/Performance", + "P0787": "Shift/Timing Solenoid Low", + "P0788": "Shift/Timing Solenoid High", + "P0789": "Shift/Timing Solenoid Intermittent", + "P0790": "Normal/Performance Switch Circuit", + "P0791": "Intermediate Shaft Speed Sensor 'A' Circuit", + "P0792": "Intermediate Shaft Speed Sensor 'A' Circuit Range/Performance", + "P0793": "Intermediate Shaft Speed Sensor 'A' Circuit No Signal", + "P0794": "Intermediate Shaft Speed Sensor 'A' Circuit Intermittent", + "P0795": "Pressure Control Solenoid 'C'", + "P0796": "Pressure Control Solenoid 'C' Performance or Stuck off", + "P0797": "Pressure Control Solenoid 'C' Stuck On", + "P0798": "Pressure Control Solenoid 'C' Electrical", + "P0799": "Pressure Control Solenoid 'C' Intermittent", + "P0800": "Transfer Case Control System (MIL Request)", + "P0801": "Reverse Inhibit Control Circuit", + "P0802": "Transmission Control System MIL Request Circuit/Open", + "P0803": "1-4 Upshift (Skip Shift) Solenoid Control Circuit", + "P0804": "1-4 Upshift (Skip Shift) Lamp Control Circuit", + "P0805": "Clutch Position Sensor Circuit", + "P0806": "Clutch Position Sensor Circuit Range/Performance", + "P0807": "Clutch Position Sensor Circuit Low", + "P0808": "Clutch Position Sensor Circuit High", + "P0809": "Clutch Position Sensor Circuit Intermittent", + "P0810": "Clutch Position Control Error", + "P0811": "Excessive Clutch Slippage", + "P0812": "Reverse Input Circuit", + "P0813": "Reverse Output Circuit", + "P0814": "Transmission Range Display Circuit", + "P0815": "Upshift Switch Circuit", + "P0816": "Downshift Switch Circuit", + "P0817": "Starter Disable Circuit", + "P0818": "Driveline Disconnect Switch Input Circuit", + "P0819": "Up and Down Shift Switch to Transmission Range Correlation", + "P0820": "Gear Lever X-Y Position Sensor Circuit", + "P0821": "Gear Lever X Position Circuit", + "P0822": "Gear Lever Y Position Circuit", + "P0823": "Gear Lever X Position Circuit Intermittent", + "P0824": "Gear Lever Y Position Circuit Intermittent", + "P0825": "Gear Lever Push-Pull Switch (Shift Anticipate)", + "P0826": "Up and Down Shift Switch Circuit", + "P0827": "Up and Down Shift Switch Circuit Low", + "P0828": "Up and Down Shift Switch Circuit High", + "P0829": "5-6 Shift", + "P0830": "Clutch Pedal Switch 'A' Circuit", + "P0831": "Clutch Pedal Switch 'A' Circuit Low", + "P0832": "Clutch Pedal Switch 'A' Circuit High", + "P0833": "Clutch Pedal Switch 'B' Circuit", + "P0834": "Clutch Pedal Switch 'B' Circuit Low", + "P0835": "Clutch Pedal Switch 'B' Circuit High", + "P0836": "Four Wheel Drive (4WD) Switch Circuit", + "P0837": "Four Wheel Drive (4WD) Switch Circuit Range/Performance", + "P0838": "Four Wheel Drive (4WD) Switch Circuit Low", + "P0839": "Four Wheel Drive (4WD) Switch Circuit High", + "P0840": "Transmission Fluid Pressure Sensor/Switch 'A' Circuit", + "P0841": "Transmission Fluid Pressure Sensor/Switch 'A' Circuit Range/Performance", + "P0842": "Transmission Fluid Pressure Sensor/Switch 'A' Circuit Low", + "P0843": "Transmission Fluid Pressure Sensor/Switch 'A' Circuit High", + "P0844": "Transmission Fluid Pressure Sensor/Switch 'A' Circuit Intermittent", + "P0845": "Transmission Fluid Pressure Sensor/Switch 'B' Circuit", + "P0846": "Transmission Fluid Pressure Sensor/Switch 'B' Circuit Range/Performance", + "P0847": "Transmission Fluid Pressure Sensor/Switch 'B' Circuit Low", + "P0848": "Transmission Fluid Pressure Sensor/Switch 'B' Circuit High", + "P0849": "Transmission Fluid Pressure Sensor/Switch 'B' Circuit Intermittent", + "P0850": "Park/Neutral Switch Input Circuit", + "P0851": "Park/Neutral Switch Input Circuit Low", + "P0852": "Park/Neutral Switch Input Circuit High", + "P0853": "Drive Switch Input Circuit", + "P0854": "Drive Switch Input Circuit Low", + "P0855": "Drive Switch Input Circuit High", + "P0856": "Traction Control Input Signal", + "P0857": "Traction Control Input Signal Range/Performance", + "P0858": "Traction Control Input Signal Low", + "P0859": "Traction Control Input Signal High", + "P0860": "Gear Shift Module Communication Circuit", + "P0861": "Gear Shift Module Communication Circuit Low", + "P0862": "Gear Shift Module Communication Circuit High", + "P0863": "TCM Communication Circuit", + "P0864": "TCM Communication Circuit Range/Performance", + "P0865": "TCM Communication Circuit Low", + "P0866": "TCM Communication Circuit High", + "P0867": "Transmission Fluid Pressure", + "P0868": "Transmission Fluid Pressure Low", + "P0869": "Transmission Fluid Pressure High", + "P0870": "Transmission Fluid Pressure Sensor/Switch 'C' Circuit", + "P0871": "Transmission Fluid Pressure Sensor/Switch 'C' Circuit Range/Performance", + "P0872": "Transmission Fluid Pressure Sensor/Switch 'C' Circuit Low", + "P0873": "Transmission Fluid Pressure Sensor/Switch 'C' Circuit High", + "P0874": "Transmission Fluid Pressure Sensor/Switch 'C' Circuit Intermittent", + "P0875": "Transmission Fluid Pressure Sensor/Switch 'D' Circuit", + "P0876": "Transmission Fluid Pressure Sensor/Switch 'D' Circuit Range/Performance", + "P0877": "Transmission Fluid Pressure Sensor/Switch 'D' Circuit Low", + "P0878": "Transmission Fluid Pressure Sensor/Switch 'D' Circuit High", + "P0879": "Transmission Fluid Pressure Sensor/Switch 'D' Circuit Intermittent", + "P0880": "TCM Power Input Signal", + "P0881": "TCM Power Input Signal Range/Performance", + "P0882": "TCM Power Input Signal Low", + "P0883": "TCM Power Input Signal High", + "P0884": "TCM Power Input Signal Intermittent", + "P0885": "TCM Power Relay Control Circuit/Open", + "P0886": "TCM Power Relay Control Circuit Low", + "P0887": "TCM Power Relay Control Circuit High", + "P0888": "TCM Power Relay Sense Circuit", + "P0889": "TCM Power Relay Sense Circuit Range/Performance", + "P0890": "TCM Power Relay Sense Circuit Low", + "P0891": "TCM Power Relay Sense Circuit High", + "P0892": "TCM Power Relay Sense Circuit Intermittent", + "P0893": "Multiple Gears Engaged", + "P0894": "Transmission Component Slipping", + "P0895": "Shift Time Too Short", + "P0896": "Shift Time Too Long", + "P0897": "Transmission Fluid Deteriorated", + "P0898": "Transmission Control System MIL Request Circuit Low", + "P0899": "Transmission Control System MIL Request Circuit High", + "P0900": "Clutch Actuator Circuit/Open", + "P0901": "Clutch Actuator Circuit Range/Performance", + "P0902": "Clutch Actuator Circuit Low", + "P0903": "Clutch Actuator Circuit High", + "P0904": "Gate Select Position Circuit", + "P0905": "Gate Select Position Circuit Range/Performance", + "P0906": "Gate Select Position Circuit Low", + "P0907": "Gate Select Position Circuit High", + "P0908": "Gate Select Position Circuit Intermittent", + "P0909": "Gate Select Control Error", + "P0910": "Gate Select Actuator Circuit/Open", + "P0911": "Gate Select Actuator Circuit Range/Performance", + "P0912": "Gate Select Actuator Circuit Low", + "P0913": "Gate Select Actuator Circuit High", + "P0914": "Gear Shift Position Circuit", + "P0915": "Gear Shift Position Circuit Range/Performance", + "P0916": "Gear Shift Position Circuit Low", + "P0917": "Gear Shift Position Circuit High", + "P0918": "Gear Shift Position Circuit Intermittent", + "P0919": "Gear Shift Position Control Error", + "P0920": "Gear Shift Forward Actuator Circuit/Open", + "P0921": "Gear Shift Forward Actuator Circuit Range/Performance", + "P0922": "Gear Shift Forward Actuator Circuit Low", + "P0923": "Gear Shift Forward Actuator Circuit High", + "P0924": "Gear Shift Reverse Actuator Circuit/Open", + "P0925": "Gear Shift Reverse Actuator Circuit Range/Performance", + "P0926": "Gear Shift Reverse Actuator Circuit Low", + "P0927": "Gear Shift Reverse Actuator Circuit High", + "P0928": "Gear Shift Lock Solenoid Control Circuit/Open", + "P0929": "Gear Shift Lock Solenoid Control Circuit Range/Performance", + "P0930": "Gear Shift Lock Solenoid Control Circuit Low", + "P0931": "Gear Shift Lock Solenoid Control Circuit High", + "P0932": "Hydraulic Pressure Sensor Circuit", + "P0933": "Hydraulic Pressure Sensor Range/Performance", + "P0934": "Hydraulic Pressure Sensor Circuit Low", + "P0935": "Hydraulic Pressure Sensor Circuit High", + "P0936": "Hydraulic Pressure Sensor Circuit Intermittent", + "P0937": "Hydraulic Oil Temperature Sensor Circuit", + "P0938": "Hydraulic Oil Temperature Sensor Range/Performance", + "P0939": "Hydraulic Oil Temperature Sensor Circuit Low", + "P0940": "Hydraulic Oil Temperature Sensor Circuit High", + "P0941": "Hydraulic Oil Temperature Sensor Circuit Intermittent", + "P0942": "Hydraulic Pressure Unit", + "P0943": "Hydraulic Pressure Unit Cycling Period Too Short", + "P0944": "Hydraulic Pressure Unit Loss of Pressure", + "P0945": "Hydraulic Pump Relay Circuit/Open", + "P0946": "Hydraulic Pump Relay Circuit Range/Performance", + "P0947": "Hydraulic Pump Relay Circuit Low", + "P0948": "Hydraulic Pump Relay Circuit High", + "P0949": "Auto Shift Manual Adaptive Learning Not Complete", + "P0950": "Auto Shift Manual Control Circuit", + "P0951": "Auto Shift Manual Control Circuit Range/Performance", + "P0952": "Auto Shift Manual Control Circuit Low", + "P0953": "Auto Shift Manual Control Circuit High", + "P0954": "Auto Shift Manual Control Circuit Intermittent", + "P0955": "Auto Shift Manual Mode Circuit", + "P0956": "Auto Shift Manual Mode Circuit Range/Performance", + "P0957": "Auto Shift Manual Mode Circuit Low", + "P0958": "Auto Shift Manual Mode Circuit High", + "P0959": "Auto Shift Manual Mode Circuit Intermittent", + "P0960": "Pressure Control Solenoid 'A' Control Circuit/Open", + "P0961": "Pressure Control Solenoid 'A' Control Circuit Range/Performance", + "P0962": "Pressure Control Solenoid 'A' Control Circuit Low", + "P0963": "Pressure Control Solenoid 'A' Control Circuit High", + "P0964": "Pressure Control Solenoid 'B' Control Circuit/Open", + "P0965": "Pressure Control Solenoid 'B' Control Circuit Range/Performance", + "P0966": "Pressure Control Solenoid 'B' Control Circuit Low", + "P0967": "Pressure Control Solenoid 'B' Control Circuit High", + "P0968": "Pressure Control Solenoid 'C' Control Circuit/Open", + "P0969": "Pressure Control Solenoid 'C' Control Circuit Range/Performance", + "P0970": "Pressure Control Solenoid 'C' Control Circuit Low", + "P0971": "Pressure Control Solenoid 'C' Control Circuit High", + "P0972": "Shift Solenoid 'A' Control Circuit Range/Performance", + "P0973": "Shift Solenoid 'A' Control Circuit Low", + "P0974": "Shift Solenoid 'A' Control Circuit High", + "P0975": "Shift Solenoid 'B' Control Circuit Range/Performance", + "P0976": "Shift Solenoid 'B' Control Circuit Low", + "P0977": "Shift Solenoid 'B' Control Circuit High", + "P0978": "Shift Solenoid 'C' Control Circuit Range/Performance", + "P0979": "Shift Solenoid 'C' Control Circuit Low", + "P0980": "Shift Solenoid 'C' Control Circuit High", + "P0981": "Shift Solenoid 'D' Control Circuit Range/Performance", + "P0982": "Shift Solenoid 'D' Control Circuit Low", + "P0983": "Shift Solenoid 'D' Control Circuit High", + "P0984": "Shift Solenoid 'E' Control Circuit Range/Performance", + "P0985": "Shift Solenoid 'E' Control Circuit Low", + "P0986": "Shift Solenoid 'E' Control Circuit High", + "P0987": "Transmission Fluid Pressure Sensor/Switch 'E' Circuit", + "P0988": "Transmission Fluid Pressure Sensor/Switch 'E' Circuit Range/Performance", + "P0989": "Transmission Fluid Pressure Sensor/Switch 'E' Circuit Low", + "P0990": "Transmission Fluid Pressure Sensor/Switch 'E' Circuit High", + "P0991": "Transmission Fluid Pressure Sensor/Switch 'E' Circuit Intermittent", + "P0992": "Transmission Fluid Pressure Sensor/Switch 'F' Circuit", + "P0993": "Transmission Fluid Pressure Sensor/Switch 'F' Circuit Range/Performance", + "P0994": "Transmission Fluid Pressure Sensor/Switch 'F' Circuit Low", + "P0995": "Transmission Fluid Pressure Sensor/Switch 'F' Circuit High", + "P0996": "Transmission Fluid Pressure Sensor/Switch 'F' Circuit Intermittent", + "P0997": "Shift Solenoid 'F' Control Circuit Range/Performance", + "P0998": "Shift Solenoid 'F' Control Circuit Low", + "P0999": "Shift Solenoid 'F' Control Circuit High", + "P0A00": "Motor Electronics Coolant Temperature Sensor Circuit", + "P0A01": "Motor Electronics Coolant Temperature Sensor Circuit Range/Performance", + "P0A02": "Motor Electronics Coolant Temperature Sensor Circuit Low", + "P0A03": "Motor Electronics Coolant Temperature Sensor Circuit High", + "P0A04": "Motor Electronics Coolant Temperature Sensor Circuit Intermittent", + "P0A05": "Motor Electronics Coolant Pump Control Circuit/Open", + "P0A06": "Motor Electronics Coolant Pump Control Circuit Low", + "P0A07": "Motor Electronics Coolant Pump Control Circuit High", + "P0A08": "DC/DC Converter Status Circuit", + "P0A09": "DC/DC Converter Status Circuit Low Input", + "P0A10": "DC/DC Converter Status Circuit High Input", + "P0A11": "DC/DC Converter Enable Circuit/Open", + "P0A12": "DC/DC Converter Enable Circuit Low", + "P0A13": "DC/DC Converter Enable Circuit High", + "P0A14": "Engine Mount Control Circuit/Open", + "P0A15": "Engine Mount Control Circuit Low", + "P0A16": "Engine Mount Control Circuit High", + "P0A17": "Motor Torque Sensor Circuit", + "P0A18": "Motor Torque Sensor Circuit Range/Performance", + "P0A19": "Motor Torque Sensor Circuit Low", + "P0A20": "Motor Torque Sensor Circuit High", + "P0A21": "Motor Torque Sensor Circuit Intermittent", + "P0A22": "Generator Torque Sensor Circuit", + "P0A23": "Generator Torque Sensor Circuit Range/Performance", + "P0A24": "Generator Torque Sensor Circuit Low", + "P0A25": "Generator Torque Sensor Circuit High", + "P0A26": "Generator Torque Sensor Circuit Intermittent", + "P0A27": "Battery Power Off Circuit", + "P0A28": "Battery Power Off Circuit Low", + "P0A29": "Battery Power Off Circuit High", + "P2000": "NOx Trap Efficiency Below Threshold", + "P2001": "NOx Trap Efficiency Below Threshold", + "P2002": "Particulate Trap Efficiency Below Threshold", + "P2003": "Particulate Trap Efficiency Below Threshold", + "P2004": "Intake Manifold Runner Control Stuck Open", + "P2005": "Intake Manifold Runner Control Stuck Open", + "P2006": "Intake Manifold Runner Control Stuck Closed", + "P2007": "Intake Manifold Runner Control Stuck Closed", + "P2008": "Intake Manifold Runner Control Circuit/Open", + "P2009": "Intake Manifold Runner Control Circuit Low", + "P2010": "Intake Manifold Runner Control Circuit High", + "P2011": "Intake Manifold Runner Control Circuit/Open", + "P2012": "Intake Manifold Runner Control Circuit Low", + "P2013": "Intake Manifold Runner Control Circuit High", + "P2014": "Intake Manifold Runner Position Sensor/Switch Circuit", + "P2015": "Intake Manifold Runner Position Sensor/Switch Circuit Range/Performance", + "P2016": "Intake Manifold Runner Position Sensor/Switch Circuit Low", + "P2017": "Intake Manifold Runner Position Sensor/Switch Circuit High", + "P2018": "Intake Manifold Runner Position Sensor/Switch Circuit Intermittent", + "P2019": "Intake Manifold Runner Position Sensor/Switch Circuit", + "P2020": "Intake Manifold Runner Position Sensor/Switch Circuit Range/Performance", + "P2021": "Intake Manifold Runner Position Sensor/Switch Circuit Low", + "P2022": "Intake Manifold Runner Position Sensor/Switch Circuit High", + "P2023": "Intake Manifold Runner Position Sensor/Switch Circuit Intermittent", + "P2024": "Evaporative Emissions (EVAP) Fuel Vapor Temperature Sensor Circuit", + "P2025": "Evaporative Emissions (EVAP) Fuel Vapor Temperature Sensor Performance", + "P2026": "Evaporative Emissions (EVAP) Fuel Vapor Temperature Sensor Circuit Low Voltage", + "P2027": "Evaporative Emissions (EVAP) Fuel Vapor Temperature Sensor Circuit High Voltage", + "P2028": "Evaporative Emissions (EVAP) Fuel Vapor Temperature Sensor Circuit Intermittent", + "P2029": "Fuel Fired Heater Disabled", + "P2030": "Fuel Fired Heater Performance", + "P2031": "Exhaust Gas Temperature Sensor Circuit", + "P2032": "Exhaust Gas Temperature Sensor Circuit Low", + "P2033": "Exhaust Gas Temperature Sensor Circuit High", + "P2034": "Exhaust Gas Temperature Sensor Circuit", + "P2035": "Exhaust Gas Temperature Sensor Circuit Low", + "P2036": "Exhaust Gas Temperature Sensor Circuit High", + "P2037": "Reductant Injection Air Pressure Sensor Circuit", + "P2038": "Reductant Injection Air Pressure Sensor Circuit Range/Performance", + "P2039": "Reductant Injection Air Pressure Sensor Circuit Low Input", + "P2040": "Reductant Injection Air Pressure Sensor Circuit High Input", + "P2041": "Reductant Injection Air Pressure Sensor Circuit Intermittent", + "P2042": "Reductant Temperature Sensor Circuit", + "P2043": "Reductant Temperature Sensor Circuit Range/Performance", + "P2044": "Reductant Temperature Sensor Circuit Low Input", + "P2045": "Reductant Temperature Sensor Circuit High Input", + "P2046": "Reductant Temperature Sensor Circuit Intermittent", + "P2047": "Reductant Injector Circuit/Open", + "P2048": "Reductant Injector Circuit Low", + "P2049": "Reductant Injector Circuit High", + "P2050": "Reductant Injector Circuit/Open", + "P2051": "Reductant Injector Circuit Low", + "P2052": "Reductant Injector Circuit High", + "P2053": "Reductant Injector Circuit/Open", + "P2054": "Reductant Injector Circuit Low", + "P2055": "Reductant Injector Circuit High", + "P2056": "Reductant Injector Circuit/Open", + "P2057": "Reductant Injector Circuit Low", + "P2058": "Reductant Injector Circuit High", + "P2059": "Reductant Injection Air Pump Control Circuit/Open", + "P2060": "Reductant Injection Air Pump Control Circuit Low", + "P2061": "Reductant Injection Air Pump Control Circuit High", + "P2062": "Reductant Supply Control Circuit/Open", + "P2063": "Reductant Supply Control Circuit Low", + "P2064": "Reductant Supply Control Circuit High", + "P2065": "Fuel Level Sensor 'B' Circuit", + "P2066": "Fuel Level Sensor 'B' Performance", + "P2067": "Fuel Level Sensor 'B' Circuit Low", + "P2068": "Fuel Level Sensor 'B' Circuit High", + "P2069": "Fuel Level Sensor 'B' Circuit Intermittent", + "P2070": "Intake Manifold Tuning (IMT) Valve Stuck Open", + "P2071": "Intake Manifold Tuning (IMT) Valve Stuck Closed", + "P2075": "Intake Manifold Tuning (IMT) Valve Position Sensor/Switch Circuit", + "P2076": "Intake Manifold Tuning (IMT) Valve Position Sensor/Switch Circuit Range/Performance", + "P2077": "Intake Manifold Tuning (IMT) Valve Position Sensor/Switch Circuit Low", + "P2078": "Intake Manifold Tuning (IMT) Valve Position Sensor/Switch Circuit High", + "P2079": "Intake Manifold Tuning (IMT) Valve Position Sensor/Switch Circuit Intermittent", + "P2080": "Exhaust Gas Temperature Sensor Circuit Range/Performance", + "P2081": "Exhaust Gas Temperature Sensor Circuit Intermittent", + "P2082": "Exhaust Gas Temperature Sensor Circuit Range/Performance", + "P2083": "Exhaust Gas Temperature Sensor Circuit Intermittent", + "P2084": "Exhaust Gas Temperature Sensor Circuit Range/Performance", + "P2085": "Exhaust Gas Temperature Sensor Circuit Intermittent", + "P2086": "Exhaust Gas Temperature Sensor Circuit Range/Performance", + "P2087": "Exhaust Gas Temperature Sensor Circuit Intermittent", + "P2088": "'A' Camshaft Position Actuator Control Circuit Low", + "P2089": "'A' Camshaft Position Actuator Control Circuit High", + "P2090": "'B' Camshaft Position Actuator Control Circuit Low", + "P2091": "'B' Camshaft Position Actuator Control Circuit High", + "P2092": "'A' Camshaft Position Actuator Control Circuit Low", + "P2093": "'A' Camshaft Position Actuator Control Circuit High", + "P2094": "'B' Camshaft Position Actuator Control Circuit Low", + "P2095": "'B' Camshaft Position Actuator Control Circuit High", + "P2096": "Post Catalyst Fuel Trim System Too Lean", + "P2097": "Post Catalyst Fuel Trim System Too Rich", + "P2098": "Post Catalyst Fuel Trim System Too Lean", + "P2099": "Post Catalyst Fuel Trim System Too Rich", + "P2100": "Throttle Actuator Control Motor Circuit/Open", + "P2101": "Throttle Actuator Control Motor Circuit Range/Performance", + "P2102": "Throttle Actuator Control Motor Circuit Low", + "P2103": "Throttle Actuator Control Motor Circuit High", + "P2104": "Throttle Actuator Control System - Forced Idle", + "P2105": "Throttle Actuator Control System - Forced Engine Shutdown", + "P2106": "Throttle Actuator Control System - Forced Limited Power", + "P2107": "Throttle Actuator Control Module Processor", + "P2108": "Throttle Actuator Control Module Performance", + "P2109": "Throttle/Pedal Position Sensor 'A' Minimum Stop Performance", + "P2110": "Throttle Actuator Control System - Forced Limited RPM", + "P2111": "Throttle Actuator Control System - Stuck Open", + "P2112": "Throttle Actuator Control System - Stuck Closed", + "P2113": "Throttle/Pedal Position Sensor 'B' Minimum Stop Performance", + "P2114": "Throttle/Pedal Position Sensor 'C' Minimum Stop Performance", + "P2115": "Throttle/Pedal Position Sensor 'D' Minimum Stop Performance", + "P2116": "Throttle/Pedal Position Sensor 'E' Minimum Stop Performance", + "P2117": "Throttle/Pedal Position Sensor 'F' Minimum Stop Performance", + "P2118": "Throttle Actuator Control Motor Current Range/Performance", + "P2119": "Throttle Actuator Control Throttle Body Range/Performance", + "P2120": "Throttle/Pedal Position Sensor/Switch 'D' Circuit", + "P2121": "Throttle/Pedal Position Sensor/Switch 'D' Circuit Range/Performance", + "P2122": "Throttle/Pedal Position Sensor/Switch 'D' Circuit Low Input", + "P2123": "Throttle/Pedal Position Sensor/Switch 'D' Circuit High Input", + "P2124": "Throttle/Pedal Position Sensor/Switch 'D' Circuit Intermittent", + "P2125": "Throttle/Pedal Position Sensor/Switch 'E' Circuit", + "P2126": "Throttle/Pedal Position Sensor/Switch 'E' Circuit Range/Performance", + "P2127": "Throttle/Pedal Position Sensor/Switch 'E' Circuit Low Input", + "P2128": "Throttle/Pedal Position Sensor/Switch 'E' Circuit High Input", + "P2129": "Throttle/Pedal Position Sensor/Switch 'E' Circuit Intermittent", + "P2130": "Throttle/Pedal Position Sensor/Switch 'F' Circuit", + "P2131": "Throttle/Pedal Position Sensor/Switch 'F' Circuit Range Performance", + "P2132": "Throttle/Pedal Position Sensor/Switch 'F' Circuit Low Input", + "P2133": "Throttle/Pedal Position Sensor/Switch 'F' Circuit High Input", + "P2134": "Throttle/Pedal Position Sensor/Switch 'F' Circuit Intermittent", + "P2135": "Throttle/Pedal Position Sensor/Switch 'A' / 'B' Voltage Correlation", + "P2136": "Throttle/Pedal Position Sensor/Switch 'A' / 'C' Voltage Correlation", + "P2137": "Throttle/Pedal Position Sensor/Switch 'B' / 'C' Voltage Correlation", + "P2138": "Throttle/Pedal Position Sensor/Switch 'D' / 'E' Voltage Correlation", + "P2139": "Throttle/Pedal Position Sensor/Switch 'D' / 'F' Voltage Correlation", + "P2140": "Throttle/Pedal Position Sensor/Switch 'E' / 'F' Voltage Correlation", + "P2141": "Exhaust Gas Recirculation Throttle Control Circuit Low", + "P2142": "Exhaust Gas Recirculation Throttle Control Circuit High", + "P2143": "Exhaust Gas Recirculation Vent Control Circuit/Open", + "P2144": "Exhaust Gas Recirculation Vent Control Circuit Low", + "P2145": "Exhaust Gas Recirculation Vent Control Circuit High", + "P2146": "Fuel Injector Group 'A' Supply Voltage Circuit/Open", + "P2147": "Fuel Injector Group 'A' Supply Voltage Circuit Low", + "P2148": "Fuel Injector Group 'A' Supply Voltage Circuit High", + "P2149": "Fuel Injector Group 'B' Supply Voltage Circuit/Open", + "P2150": "Fuel Injector Group 'B' Supply Voltage Circuit Low", + "P2151": "Fuel Injector Group 'B' Supply Voltage Circuit High", + "P2152": "Fuel Injector Group 'C' Supply Voltage Circuit/Open", + "P2153": "Fuel Injector Group 'C' Supply Voltage Circuit Low", + "P2154": "Fuel Injector Group 'C' Supply Voltage Circuit High", + "P2155": "Fuel Injector Group 'D' Supply Voltage Circuit/Open", + "P2156": "Fuel Injector Group 'D' Supply Voltage Circuit Low", + "P2157": "Fuel Injector Group 'D' Supply Voltage Circuit High", + "P2158": "Vehicle Speed Sensor 'B'", + "P2159": "Vehicle Speed Sensor 'B' Range/Performance", + "P2160": "Vehicle Speed Sensor 'B' Circuit Low", + "P2161": "Vehicle Speed Sensor 'B' Intermittent/Erratic", + "P2162": "Vehicle Speed Sensor 'A' / 'B' Correlation", + "P2163": "Throttle/Pedal Position Sensor 'A' Maximum Stop Performance", + "P2164": "Throttle/Pedal Position Sensor 'B' Maximum Stop Performance", + "P2165": "Throttle/Pedal Position Sensor 'C' Maximum Stop Performance", + "P2166": "Throttle/Pedal Position Sensor 'D' Maximum Stop Performance", + "P2167": "Throttle/Pedal Position Sensor 'E' Maximum Stop Performance", + "P2168": "Throttle/Pedal Position Sensor 'F' Maximum Stop Performance", + "P2169": "Exhaust Pressure Regulator Vent Solenoid Control Circuit/Open", + "P2170": "Exhaust Pressure Regulator Vent Solenoid Control Circuit Low", + "P2171": "Exhaust Pressure Regulator Vent Solenoid Control Circuit High", + "P2172": "Throttle Actuator Control System - Sudden High Airflow Detected", + "P2173": "Throttle Actuator Control System - High Airflow Detected", + "P2174": "Throttle Actuator Control System - Sudden Low Airflow Detected", + "P2175": "Throttle Actuator Control System - Low Airflow Detected", + "P2176": "Throttle Actuator Control System - Idle Position Not Learned", + "P2177": "System Too Lean Off Idle", + "P2178": "System Too Rich Off Idle", + "P2179": "System Too Lean Off Idle", + "P2180": "System Too Rich Off Idle", + "P2181": "Cooling System Performance", + "P2182": "Engine Coolant Temperature Sensor 2 Circuit", + "P2183": "Engine Coolant Temperature Sensor 2 Circuit Range/Performance", + "P2184": "Engine Coolant Temperature Sensor 2 Circuit Low", + "P2185": "Engine Coolant Temperature Sensor 2 Circuit High", + "P2186": "Engine Coolant Temperature Sensor 2 Circuit Intermittent/Erratic", + "P2187": "System Too Lean at Idle", + "P2188": "System Too Rich at Idle", + "P2189": "System Too Lean at Idle", + "P2190": "System Too Rich at Idle", + "P2191": "System Too Lean at Higher Load", + "P2192": "System Too Rich at Higher Load", + "P2193": "System Too Lean at Higher Load", + "P2194": "System Too Rich at Higher Load", + "P2195": "O2 Sensor Signal Stuck Lean", + "P2196": "O2 Sensor Signal Stuck Rich", + "P2197": "O2 Sensor Signal Stuck Lean", + "P2198": "O2 Sensor Signal Stuck Rich", + "P2199": "Intake Air Temperature Sensor 1 / 2 Correlation", + "P2200": "NOx Sensor Circuit", + "P2201": "NOx Sensor Circuit Range/Performance", + "P2202": "NOx Sensor Circuit Low Input", + "P2203": "NOx Sensor Circuit High Input", + "P2204": "NOx Sensor Circuit Intermittent Input", + "P2205": "NOx Sensor Heater Control Circuit/Open", + "P2206": "NOx Sensor Heater Control Circuit Low", + "P2207": "NOx Sensor Heater Control Circuit High", + "P2208": "NOx Sensor Heater Sense Circuit", + "P2209": "NOx Sensor Heater Sense Circuit Range/Performance", + "P2210": "NOx Sensor Heater Sense Circuit Low Input", + "P2211": "NOx Sensor Heater Sense Circuit High Input", + "P2212": "NOx Sensor Heater Sense Circuit Intermittent", + "P2213": "NOx Sensor Circuit", + "P2214": "NOx Sensor Circuit Range/Performance", + "P2215": "NOx Sensor Circuit Low Input", + "P2216": "NOx Sensor Circuit High Input", + "P2217": "NOx Sensor Circuit Intermittent Input", + "P2218": "NOx Sensor Heater Control Circuit/Open", + "P2219": "NOx Sensor Heater Control Circuit Low", + "P2220": "NOx Sensor Heater Control Circuit High", + "P2221": "NOx Sensor Heater Sense Circuit", + "P2222": "NOx Sensor Heater Sense Circuit Range/Performance", + "P2223": "NOx Sensor Heater Sense Circuit Low", + "P2224": "NOx Sensor Heater Sense Circuit High", + "P2225": "NOx Sensor Heater Sense Circuit Intermittent", + "P2226": "Barometric Pressure Circuit", + "P2227": "Barometric Pressure Circuit Range/Performance", + "P2228": "Barometric Pressure Circuit Low", + "P2229": "Barometric Pressure Circuit High", + "P2230": "Barometric Pressure Circuit Intermittent", + "P2231": "O2 Sensor Signal Circuit Shorted to Heater Circuit", + "P2232": "O2 Sensor Signal Circuit Shorted to Heater Circuit", + "P2233": "O2 Sensor Signal Circuit Shorted to Heater Circuit", + "P2234": "O2 Sensor Signal Circuit Shorted to Heater Circuit", + "P2235": "O2 Sensor Signal Circuit Shorted to Heater Circuit", + "P2236": "O2 Sensor Signal Circuit Shorted to Heater Circuit", + "P2237": "O2 Sensor Positive Current Control Circuit/Open", + "P2238": "O2 Sensor Positive Current Control Circuit Low", + "P2239": "O2 Sensor Positive Current Control Circuit High", + "P2240": "O2 Sensor Positive Current Control Circuit/Open", + "P2241": "O2 Sensor Positive Current Control Circuit Low", + "P2242": "O2 Sensor Positive Current Control Circuit High", + "P2243": "O2 Sensor Reference Voltage Circuit/Open", + "P2244": "O2 Sensor Reference Voltage Performance", + "P2245": "O2 Sensor Reference Voltage Circuit Low", + "P2246": "O2 Sensor Reference Voltage Circuit High", + "P2247": "O2 Sensor Reference Voltage Circuit/Open", + "P2248": "O2 Sensor Reference Voltage Performance", + "P2249": "O2 Sensor Reference Voltage Circuit Low", + "P2250": "O2 Sensor Reference Voltage Circuit High", + "P2251": "O2 Sensor Negative Current Control Circuit/Open", + "P2252": "O2 Sensor Negative Current Control Circuit Low", + "P2253": "O2 Sensor Negative Current Control Circuit High", + "P2254": "O2 Sensor Negative Current Control Circuit/Open", + "P2255": "O2 Sensor Negative Current Control Circuit Low", + "P2256": "O2 Sensor Negative Current Control Circuit High", + "P2257": "Secondary Air Injection System Control 'A' Circuit Low", + "P2258": "Secondary Air Injection System Control 'A' Circuit High", + "P2259": "Secondary Air Injection System Control 'B' Circuit Low", + "P2260": "Secondary Air Injection System Control 'B' Circuit High", + "P2261": "Turbo/Super Charger Bypass Valve - Mechanical", + "P2262": "Turbo Boost Pressure Not Detected - Mechanical", + "P2263": "Turbo/Super Charger Boost System Performance", + "P2264": "Water in Fuel Sensor Circuit", + "P2265": "Water in Fuel Sensor Circuit Range/Performance", + "P2266": "Water in Fuel Sensor Circuit Low", + "P2267": "Water in Fuel Sensor Circuit High", + "P2268": "Water in Fuel Sensor Circuit Intermittent", + "P2269": "Water in Fuel Condition", + "P2270": "O2 Sensor Signal Stuck Lean", + "P2271": "O2 Sensor Signal Stuck Rich", + "P2272": "O2 Sensor Signal Stuck Lean", + "P2273": "O2 Sensor Signal Stuck Rich", + "P2274": "O2 Sensor Signal Stuck Lean", + "P2275": "O2 Sensor Signal Stuck Rich", + "P2276": "O2 Sensor Signal Stuck Lean", + "P2277": "O2 Sensor Signal Stuck Rich", + "P2278": "O2 Sensor Signals Swapped Bank 1 Sensor 3 / Bank 2 Sensor 3", + "P2279": "Intake Air System Leak", + "P2280": "Air Flow Restriction / Air Leak Between Air Filter and MAF", + "P2281": "Air Leak Between MAF and Throttle Body", + "P2282": "Air Leak Between Throttle Body and Intake Valves", + "P2283": "Injector Control Pressure Sensor Circuit", + "P2284": "Injector Control Pressure Sensor Circuit Range/Performance", + "P2285": "Injector Control Pressure Sensor Circuit Low", + "P2286": "Injector Control Pressure Sensor Circuit High", + "P2287": "Injector Control Pressure Sensor Circuit Intermittent", + "P2288": "Injector Control Pressure Too High", + "P2289": "Injector Control Pressure Too High - Engine Off", + "P2290": "Injector Control Pressure Too Low", + "P2291": "Injector Control Pressure Too Low - Engine Cranking", + "P2292": "Injector Control Pressure Erratic", + "P2293": "Fuel Pressure Regulator 2 Performance", + "P2294": "Fuel Pressure Regulator 2 Control Circuit", + "P2295": "Fuel Pressure Regulator 2 Control Circuit Low", + "P2296": "Fuel Pressure Regulator 2 Control Circuit High", + "P2297": "O2 Sensor Out of Range During Deceleration", + "P2298": "O2 Sensor Out of Range During Deceleration", + "P2299": "Brake Pedal Position / Accelerator Pedal Position Incompatible", + "P2300": "Ignition Coil 'A' Primary Control Circuit Low", + "P2301": "Ignition Coil 'A' Primary Control Circuit High", + "P2302": "Ignition Coil 'A' Secondary Circuit", + "P2303": "Ignition Coil 'B' Primary Control Circuit Low", + "P2304": "Ignition Coil 'B' Primary Control Circuit High", + "P2305": "Ignition Coil 'B' Secondary Circuit", + "P2306": "Ignition Coil 'C' Primary Control Circuit Low", + "P2307": "Ignition Coil 'C' Primary Control Circuit High", + "P2308": "Ignition Coil 'C' Secondary Circuit", + "P2309": "Ignition Coil 'D' Primary Control Circuit Low", + "P2310": "Ignition Coil 'D' Primary Control Circuit High", + "P2311": "Ignition Coil 'D' Secondary Circuit", + "P2312": "Ignition Coil 'E' Primary Control Circuit Low", + "P2313": "Ignition Coil 'E' Primary Control Circuit High", + "P2314": "Ignition Coil 'E' Secondary Circuit", + "P2315": "Ignition Coil 'F' Primary Control Circuit Low", + "P2316": "Ignition Coil 'F' Primary Control Circuit High", + "P2317": "Ignition Coil 'F' Secondary Circuit", + "P2318": "Ignition Coil 'G' Primary Control Circuit Low", + "P2319": "Ignition Coil 'G' Primary Control Circuit High", + "P2320": "Ignition Coil 'G' Secondary Circuit", + "P2321": "Ignition Coil 'H' Primary Control Circuit Low", + "P2322": "Ignition Coil 'H' Primary Control Circuit High", + "P2323": "Ignition Coil 'H' Secondary Circuit", + "P2324": "Ignition Coil 'I' Primary Control Circuit Low", + "P2325": "Ignition Coil 'I' Primary Control Circuit High", + "P2326": "Ignition Coil 'I' Secondary Circuit", + "P2327": "Ignition Coil 'J' Primary Control Circuit Low", + "P2328": "Ignition Coil 'J' Primary Control Circuit High", + "P2329": "Ignition Coil 'J' Secondary Circuit", + "P2330": "Ignition Coil 'K' Primary Control Circuit Low", + "P2331": "Ignition Coil 'K' Primary Control Circuit High", + "P2332": "Ignition Coil 'K' Secondary Circuit", + "P2333": "Ignition Coil 'L' Primary Control Circuit Low", + "P2334": "Ignition Coil 'L' Primary Control Circuit High", + "P2335": "Ignition Coil 'L' Secondary Circuit", + "P2336": "Cylinder #1 Above Knock Threshold", + "P2337": "Cylinder #2 Above Knock Threshold", + "P2338": "Cylinder #3 Above Knock Threshold", + "P2339": "Cylinder #4 Above Knock Threshold", + "P2340": "Cylinder #5 Above Knock Threshold", + "P2341": "Cylinder #6 Above Knock Threshold", + "P2342": "Cylinder #7 Above Knock Threshold", + "P2343": "Cylinder #8 Above Knock Threshold", + "P2344": "Cylinder #9 Above Knock Threshold", + "P2345": "Cylinder #10 Above Knock Threshold", + "P2346": "Cylinder #11 Above Knock Threshold", + "P2347": "Cylinder #12 Above Knock Threshold", + "P2400": "Evaporative Emission System Leak Detection Pump Control Circuit/Open", + "P2401": "Evaporative Emission System Leak Detection Pump Control Circuit Low", + "P2402": "Evaporative Emission System Leak Detection Pump Control Circuit High", + "P2403": "Evaporative Emission System Leak Detection Pump Sense Circuit/Open", + "P2404": "Evaporative Emission System Leak Detection Pump Sense Circuit Range/Performance", + "P2405": "Evaporative Emission System Leak Detection Pump Sense Circuit Low", + "P2406": "Evaporative Emission System Leak Detection Pump Sense Circuit High", + "P2407": "Evaporative Emission System Leak Detection Pump Sense Circuit Intermittent/Erratic", + "P2408": "Fuel Cap Sensor/Switch Circuit", + "P2409": "Fuel Cap Sensor/Switch Circuit Range/Performance", + "P2410": "Fuel Cap Sensor/Switch Circuit Low", + "P2411": "Fuel Cap Sensor/Switch Circuit High", + "P2412": "Fuel Cap Sensor/Switch Circuit Intermittent/Erratic", + "P2413": "Exhaust Gas Recirculation System Performance", + "P2414": "O2 Sensor Exhaust Sample Error", + "P2415": "O2 Sensor Exhaust Sample Error", + "P2416": "O2 Sensor Signals Swapped Bank 1 Sensor 2 / Bank 1 Sensor 3", + "P2417": "O2 Sensor Signals Swapped Bank 2 Sensor 2 / Bank 2 Sensor 3", + "P2418": "Evaporative Emission System Switching Valve Control Circuit / Open", + "P2419": "Evaporative Emission System Switching Valve Control Circuit Low", + "P2420": "Evaporative Emission System Switching Valve Control Circuit High", + "P2421": "Evaporative Emission System Vent Valve Stuck Open", + "P2422": "Evaporative Emission System Vent Valve Stuck Closed", + "P2423": "HC Adsorption Catalyst Efficiency Below Threshold", + "P2424": "HC Adsorption Catalyst Efficiency Below Threshold", + "P2425": "Exhaust Gas Recirculation Cooling Valve Control Circuit/Open", + "P2426": "Exhaust Gas Recirculation Cooling Valve Control Circuit Low", + "P2427": "Exhaust Gas Recirculation Cooling Valve Control Circuit High", + "P2428": "Exhaust Gas Temperature Too High", + "P2429": "Exhaust Gas Temperature Too High", + "P2430": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit", + "P2431": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit Range/Performance", + "P2432": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit Low", + "P2433": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit High", + "P2434": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit Intermittent/Erratic", + "P2435": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit", + "P2436": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit Range/Performance", + "P2437": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit Low", + "P2438": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit High", + "P2439": "Secondary Air Injection System Air Flow/Pressure Sensor Circuit Intermittent/Erratic", + "P2440": "Secondary Air Injection System Switching Valve Stuck Open", + "P2441": "Secondary Air Injection System Switching Valve Stuck Closed", + "P2442": "Secondary Air Injection System Switching Valve Stuck Open", + "P2443": "Secondary Air Injection System Switching Valve Stuck Closed", + "P2444": "Secondary Air Injection System Pump Stuck On", + "P2445": "Secondary Air Injection System Pump Stuck Off", + "P2446": "Secondary Air Injection System Pump Stuck On", + "P2447": "Secondary Air Injection System Pump Stuck Off", + "P2500": "Generator Lamp/L-Terminal Circuit Low", + "P2501": "Generator Lamp/L-Terminal Circuit High", + "P2502": "Charging System Voltage", + "P2503": "Charging System Voltage Low", + "P2504": "Charging System Voltage High", + "P2505": "ECM/PCM Power Input Signal", + "P2506": "ECM/PCM Power Input Signal Range/Performance", + "P2507": "ECM/PCM Power Input Signal Low", + "P2508": "ECM/PCM Power Input Signal High", + "P2509": "ECM/PCM Power Input Signal Intermittent", + "P2510": "ECM/PCM Power Relay Sense Circuit Range/Performance", + "P2511": "ECM/PCM Power Relay Sense Circuit Intermittent", + "P2512": "Event Data Recorder Request Circuit/ Open", + "P2513": "Event Data Recorder Request Circuit Low", + "P2514": "Event Data Recorder Request Circuit High", + "P2515": "A/C Refrigerant Pressure Sensor 'B' Circuit", + "P2516": "A/C Refrigerant Pressure Sensor 'B' Circuit Range/Performance", + "P2517": "A/C Refrigerant Pressure Sensor 'B' Circuit Low", + "P2518": "A/C Refrigerant Pressure Sensor 'B' Circuit High", + "P2519": "A/C Request 'A' Circuit", + "P2520": "A/C Request 'A' Circuit Low", + "P2521": "A/C Request 'A' Circuit High", + "P2522": "A/C Request 'B' Circuit", + "P2523": "A/C Request 'B' Circuit Low", + "P2524": "A/C Request 'B' Circuit High", + "P2525": "Vacuum Reservoir Pressure Sensor Circuit", + "P2526": "Vacuum Reservoir Pressure Sensor Circuit Range/Performance", + "P2527": "Vacuum Reservoir Pressure Sensor Circuit Low", + "P2528": "Vacuum Reservoir Pressure Sensor Circuit High", + "P2529": "Vacuum Reservoir Pressure Sensor Circuit Intermittent", + "P2530": "Ignition Switch Run Position Circuit", + "P2531": "Ignition Switch Run Position Circuit Low", + "P2532": "Ignition Switch Run Position Circuit High", + "P2533": "Ignition Switch Run/Start Position Circuit", + "P2534": "Ignition Switch Run/Start Position Circuit Low", + "P2535": "Ignition Switch Run/Start Position Circuit High", + "P2536": "Ignition Switch Accessory Position Circuit", + "P2537": "Ignition Switch Accessory Position Circuit Low", + "P2538": "Ignition Switch Accessory Position Circuit High", + "P2539": "Low Pressure Fuel System Sensor Circuit", + "P2540": "Low Pressure Fuel System Sensor Circuit Range/Performance", + "P2541": "Low Pressure Fuel System Sensor Circuit Low", + "P2542": "Low Pressure Fuel System Sensor Circuit High", + "P2543": "Low Pressure Fuel System Sensor Circuit Intermittent", + "P2544": "Torque Management Request Input Signal 'A'", + "P2545": "Torque Management Request Input Signal 'A' Range/Performance", + "P2546": "Torque Management Request Input Signal 'A' Low", + "P2547": "Torque Management Request Input Signal 'A' High", + "P2548": "Torque Management Request Input Signal 'B'", + "P2549": "Torque Management Request Input Signal 'B' Range/Performance", + "P2550": "Torque Management Request Input Signal 'B' Low", + "P2551": "Torque Management Request Input Signal 'B' High", + "P2552": "Throttle/Fuel Inhibit Circuit", + "P2553": "Throttle/Fuel Inhibit Circuit Range/Performance", + "P2554": "Throttle/Fuel Inhibit Circuit Low", + "P2555": "Throttle/Fuel Inhibit Circuit High", + "P2556": "Engine Coolant Level Sensor/Switch Circuit", + "P2557": "Engine Coolant Level Sensor/Switch Circuit Range/Performance", + "P2558": "Engine Coolant Level Sensor/Switch Circuit Low", + "P2559": "Engine Coolant Level Sensor/Switch Circuit High", + "P2560": "Engine Coolant Level Low", + "P2561": "A/C Control Module Requested MIL Illumination", + "P2562": "Turbocharger Boost Control Position Sensor Circuit", + "P2563": "Turbocharger Boost Control Position Sensor Circuit Range/Performance", + "P2564": "Turbocharger Boost Control Position Sensor Circuit Low", + "P2565": "Turbocharger Boost Control Position Sensor Circuit High", + "P2566": "Turbocharger Boost Control Position Sensor Circuit Intermittent", + "P2567": "Direct Ozone Reduction Catalyst Temperature Sensor Circuit", + "P2568": "Direct Ozone Reduction Catalyst Temperature Sensor Circuit Range/Performance", + "P2569": "Direct Ozone Reduction Catalyst Temperature Sensor Circuit Low", + "P2570": "Direct Ozone Reduction Catalyst Temperature Sensor Circuit High", + "P2571": "Direct Ozone Reduction Catalyst Temperature Sensor Circuit Intermittent/Erratic", + "P2572": "Direct Ozone Reduction Catalyst Deterioration Sensor Circuit", + "P2573": "Direct Ozone Reduction Catalyst Deterioration Sensor Circuit Range/Performance", + "P2574": "Direct Ozone Reduction Catalyst Deterioration Sensor Circuit Low", + "P2575": "Direct Ozone Reduction Catalyst Deterioration Sensor Circuit High", + "P2576": "Direct Ozone Reduction Catalyst Deterioration Sensor Circuit Intermittent/Erratic", + "P2577": "Direct Ozone Reduction Catalyst Efficiency Below Threshold", + "P2600": "Coolant Pump Control Circuit/Open", + "P2601": "Coolant Pump Control Circuit Range/Performance", + "P2602": "Coolant Pump Control Circuit Low", + "P2603": "Coolant Pump Control Circuit High", + "P2604": "Intake Air Heater 'A' Circuit Range/Performance", + "P2605": "Intake Air Heater 'A' Circuit/Open", + "P2606": "Intake Air Heater 'B' Circuit Range/Performance", + "P2607": "Intake Air Heater 'B' Circuit Low", + "P2608": "Intake Air Heater 'B' Circuit High", + "P2609": "Intake Air Heater System Performance", + "P2610": "ECM/PCM Internal Engine Off Timer Performance", + "P2611": "A/C Refrigerant Distribution Valve Control Circuit/Open", + "P2612": "A/C Refrigerant Distribution Valve Control Circuit Low", + "P2613": "A/C Refrigerant Distribution Valve Control Circuit High", + "P2614": "Camshaft Position Signal Output Circuit/Open", + "P2615": "Camshaft Position Signal Output Circuit Low", + "P2616": "Camshaft Position Signal Output Circuit High", + "P2617": "Crankshaft Position Signal Output Circuit/Open", + "P2618": "Crankshaft Position Signal Output Circuit Low", + "P2619": "Crankshaft Position Signal Output Circuit High", + "P2620": "Throttle Position Output Circuit/Open", + "P2621": "Throttle Position Output Circuit Low", + "P2622": "Throttle Position Output Circuit High", + "P2623": "Injector Control Pressure Regulator Circuit/Open", + "P2624": "Injector Control Pressure Regulator Circuit Low", + "P2625": "Injector Control Pressure Regulator Circuit High", + "P2626": "O2 Sensor Pumping Current Trim Circuit/Open", + "P2627": "O2 Sensor Pumping Current Trim Circuit Low", + "P2628": "O2 Sensor Pumping Current Trim Circuit High", + "P2629": "O2 Sensor Pumping Current Trim Circuit/Open", + "P2630": "O2 Sensor Pumping Current Trim Circuit Low", + "P2631": "O2 Sensor Pumping Current Trim Circuit High", + "P2632": "Fuel Pump 'B' Control Circuit /Open", + "P2633": "Fuel Pump 'B' Control Circuit Low", + "P2634": "Fuel Pump 'B' Control Circuit High", + "P2635": "Fuel Pump 'A' Low Flow / Performance", + "P2636": "Fuel Pump 'B' Low Flow / Performance", + "P2637": "Torque Management Feedback Signal 'A'", + "P2638": "Torque Management Feedback Signal 'A' Range/Performance", + "P2639": "Torque Management Feedback Signal 'A' Low", + "P2640": "Torque Management Feedback Signal 'A' High", + "P2641": "Torque Management Feedback Signal 'B'", + "P2642": "Torque Management Feedback Signal 'B' Range/Performance", + "P2643": "Torque Management Feedback Signal 'B' Low", + "P2644": "Torque Management Feedback Signal 'B' High", + "P2645": "'A' Rocker Arm Actuator Control Circuit/Open", + "P2646": "'A' Rocker Arm Actuator System Performance or Stuck Off", + "P2647": "'A' Rocker Arm Actuator System Stuck On", + "P2648": "'A' Rocker Arm Actuator Control Circuit Low", + "P2649": "'A' Rocker Arm Actuator Control Circuit High", + "P2650": "'B' Rocker Arm Actuator Control Circuit/Open", + "P2651": "'B' Rocker Arm Actuator System Performance or Stuck Off", + "P2652": "'B' Rocker Arm Actuator System Stuck On", + "P2653": "'B' Rocker Arm Actuator Control Circuit Low", + "P2654": "'B' Rocker Arm Actuator Control Circuit High", + "P2655": "'A' Rocker Arm Actuator Control Circuit/Open", + "P2656": "'A' Rocker Arm Actuator System Performance or Stuck Off", + "P2657": "'A' Rocker Arm Actuator System Stuck On", + "P2658": "'A' Rocker Arm Actuator Control Circuit Low", + "P2659": "'A' Rocker Arm Actuator Control Circuit High", + "P2660": "'B' Rocker Arm Actuator Control Circuit/Open", + "P2661": "'B' Rocker Arm Actuator System Performance or Stuck Off", + "P2662": "'B' Rocker Arm Actuator System Stuck On", + "P2663": "'B' Rocker Arm Actuator Control Circuit Low", + "P2664": "'B' Rocker Arm Actuator Control Circuit High", + "P2665": "Fuel Shutoff Valve 'B' Control Circuit/Open", + "P2666": "Fuel Shutoff Valve 'B' Control Circuit Low", + "P2667": "Fuel Shutoff Valve 'B' Control Circuit High", + "P2668": "Fuel Mode Indicator Lamp Control Circuit", + "P2669": "Actuator Supply Voltage 'B' Circuit /Open", + "P2670": "Actuator Supply Voltage 'B' Circuit Low", + "P2671": "Actuator Supply Voltage 'B' Circuit High", + "P2700": "Transmission Friction Element 'A' Apply Time Range/Performance", + "P2701": "Transmission Friction Element 'B' Apply Time Range/Performance", + "P2702": "Transmission Friction Element 'C' Apply Time Range/Performance", + "P2703": "Transmission Friction Element 'D' Apply Time Range/Performance", + "P2704": "Transmission Friction Element 'E' Apply Time Range/Performance", + "P2705": "Transmission Friction Element 'F' Apply Time Range/Performance", + "P2706": "Shift Solenoid 'F'", + "P2707": "Shift Solenoid 'F' Performance or Stuck Off", + "P2708": "Shift Solenoid 'F' Stuck On", + "P2709": "Shift Solenoid 'F' Electrical", + "P2710": "Shift Solenoid 'F' Intermittent", + "P2711": "Unexpected Mechanical Gear Disengagement", + "P2712": "Hydraulic Power Unit Leakage", + "P2713": "Pressure Control Solenoid 'D'", + "P2714": "Pressure Control Solenoid 'D' Performance or Stuck Off", + "P2715": "Pressure Control Solenoid 'D' Stuck On", + "P2716": "Pressure Control Solenoid 'D' Electrical", + "P2717": "Pressure Control Solenoid 'D' Intermittent", + "P2718": "Pressure Control Solenoid 'D' Control Circuit / Open", + "P2719": "Pressure Control Solenoid 'D' Control Circuit Range/Performance", + "P2720": "Pressure Control Solenoid 'D' Control Circuit Low", + "P2721": "Pressure Control Solenoid 'D' Control Circuit High", + "P2722": "Pressure Control Solenoid 'E'", + "P2723": "Pressure Control Solenoid 'E' Performance or Stuck Off", + "P2724": "Pressure Control Solenoid 'E' Stuck On", + "P2725": "Pressure Control Solenoid 'E' Electrical", + "P2726": "Pressure Control Solenoid 'E' Intermittent", + "P2727": "Pressure Control Solenoid 'E' Control Circuit / Open", + "P2728": "Pressure Control Solenoid 'E' Control Circuit Range/Performance", + "P2729": "Pressure Control Solenoid 'E' Control Circuit Low", + "P2730": "Pressure Control Solenoid 'E' Control Circuit High", + "P2731": "Pressure Control Solenoid 'F'", + "P2732": "Pressure Control Solenoid 'F' Performance or Stuck Off", + "P2733": "Pressure Control Solenoid 'F' Stuck On", + "P2734": "Pressure Control Solenoid 'F' Electrical", + "P2735": "Pressure Control Solenoid 'F' Intermittent", + "P2736": "Pressure Control Solenoid 'F' Control Circuit/Open", + "P2737": "Pressure Control Solenoid 'F' Control Circuit Range/Performance", + "P2738": "Pressure Control Solenoid 'F' Control Circuit Low", + "P2739": "Pressure Control Solenoid 'F' Control Circuit High", + "P2740": "Transmission Fluid Temperature Sensor 'B' Circuit", + "P2741": "Transmission Fluid Temperature Sensor 'B' Circuit Range Performance", + "P2742": "Transmission Fluid Temperature Sensor 'B' Circuit Low", + "P2743": "Transmission Fluid Temperature Sensor 'B' Circuit High", + "P2744": "Transmission Fluid Temperature Sensor 'B' Circuit Intermittent", + "P2745": "Intermediate Shaft Speed Sensor 'B' Circuit", + "P2746": "Intermediate Shaft Speed Sensor 'B' Circuit Range/Performance", + "P2747": "Intermediate Shaft Speed Sensor 'B' Circuit No Signal", + "P2748": "Intermediate Shaft Speed Sensor 'B' Circuit Intermittent", + "P2749": "Intermediate Shaft Speed Sensor 'C' Circuit", + "P2750": "Intermediate Shaft Speed Sensor 'C' Circuit Range/Performance", + "P2751": "Intermediate Shaft Speed Sensor 'C' Circuit No Signal", + "P2752": "Intermediate Shaft Speed Sensor 'C' Circuit Intermittent", + "P2753": "Transmission Fluid Cooler Control Circuit/Open", + "P2754": "Transmission Fluid Cooler Control Circuit Low", + "P2755": "Transmission Fluid Cooler Control Circuit High", + "P2756": "Torque Converter Clutch Pressure Control Solenoid", + "P2757": "Torque Converter Clutch Pressure Control Solenoid Control Circuit Performance or Stuck Off", + "P2758": "Torque Converter Clutch Pressure Control Solenoid Control Circuit Stuck On", + "P2759": "Torque Converter Clutch Pressure Control Solenoid Control Circuit Electrical", + "P2760": "Torque Converter Clutch Pressure Control Solenoid Control Circuit Intermittent", + "P2761": "Torque Converter Clutch Pressure Control Solenoid Control Circuit/Open", + "P2762": "Torque Converter Clutch Pressure Control Solenoid Control Circuit Range/Performance", + "P2763": "Torque Converter Clutch Pressure Control Solenoid Control Circuit High", + "P2764": "Torque Converter Clutch Pressure Control Solenoid Control Circuit Low", + "P2765": "Input/Turbine Speed Sensor 'B' Circuit", + "P2766": "Input/Turbine Speed Sensor 'B' Circuit Range/Performance", + "P2767": "Input/Turbine Speed Sensor 'B' Circuit No Signal", + "P2768": "Input/Turbine Speed Sensor 'B' Circuit Intermittent", + "P2769": "Torque Converter Clutch Circuit Low", + "P2770": "Torque Converter Clutch Circuit High", + "P2771": "Four Wheel Drive (4WD) Low Switch Circuit", + "P2772": "Four Wheel Drive (4WD) Low Switch Circuit Range/Performance", + "P2773": "Four Wheel Drive (4WD) Low Switch Circuit Low", + "P2774": "Four Wheel Drive (4WD) Low Switch Circuit High", + "P2775": "Upshift Switch Circuit Range/Performance", + "P2776": "Upshift Switch Circuit Low", + "P2777": "Upshift Switch Circuit High", + "P2778": "Upshift Switch Circuit Intermittent/Erratic", + "P2779": "Downshift Switch Circuit Range/Performance", + "P2780": "Downshift Switch Circuit Low", + "P2781": "Downshift Switch Circuit High", + "P2782": "Downshift Switch Circuit Intermittent/Erratic", + "P2783": "Torque Converter Temperature Too High", + "P2784": "Input/Turbine Speed Sensor 'A'/'B' Correlation", + "P2785": "Clutch Actuator Temperature Too High", + "P2786": "Gear Shift Actuator Temperature Too High", + "P2787": "Clutch Temperature Too High", + "P2788": "Auto Shift Manual Adaptive Learning at Limit", + "P2789": "Clutch Adaptive Learning at Limit", + "P2790": "Gate Select Direction Circuit", + "P2791": "Gate Select Direction Circuit Low", + "P2792": "Gate Select Direction Circuit High", + "P2793": "Gear Shift Direction Circuit", + "P2794": "Gear Shift Direction Circuit Low", + "P2795": "Gear Shift Direction Circuit High", + "P2A00": "O2 Sensor Circuit Range/Performance", + "P2A01": "O2 Sensor Circuit Range/Performance", + "P2A02": "O2 Sensor Circuit Range/Performance", + "P2A03": "O2 Sensor Circuit Range/Performance", + "P2A04": "O2 Sensor Circuit Range/Performance", + "P2A05": "O2 Sensor Circuit Range/Performance", + "P3400": "Cylinder Deactivation System", + "P3401": "Cylinder 1 Deactivation/lntake Valve Control Circuit/Open", + "P3402": "Cylinder 1 Deactivation/lntake Valve Control Performance", + "P3403": "Cylinder 1 Deactivation/lntake Valve Control Circuit Low", + "P3404": "Cylinder 1 Deactivation/lntake Valve Control Circuit High", + "P3405": "Cylinder 1 Exhaust Valve Control Circuit/Open", + "P3406": "Cylinder 1 Exhaust Valve Control Performance", + "P3407": "Cylinder 1 Exhaust Valve Control Circuit Low", + "P3408": "Cylinder 1 Exhaust Valve Control Circuit High", + "P3409": "Cylinder 2 Deactivation/lntake Valve Control Circuit/Open", + "P3410": "Cylinder 2 Deactivation/lntake Valve Control Performance", + "P3411": "Cylinder 2 Deactivation/lntake Valve Control Circuit Low", + "P3412": "Cylinder 2 Deactivation/lntake Valve Control Circuit High", + "P3413": "Cylinder 2 Exhaust Valve Control Circuit/Open", + "P3414": "Cylinder 2 Exhaust Valve Control Performance", + "P3415": "Cylinder 2 Exhaust Valve Control Circuit Low", + "P3416": "Cylinder 2 Exhaust Valve Control Circuit High", + "P3417": "Cylinder 3 Deactivation/lntake Valve Control Circuit/Open", + "P3418": "Cylinder 3 Deactivation/lntake Valve Control Performance", + "P3419": "Cylinder 3 Deactivation/lntake Valve Control Circuit Low", + "P3420": "Cylinder 3 Deactivation/lntake Valve Control Circuit High", + "P3421": "Cylinder 3 Exhaust Valve Control Circuit/Open", + "P3422": "Cylinder 3 Exhaust Valve Control Performance", + "P3423": "Cylinder 3 Exhaust Valve Control Circuit Low", + "P3424": "Cylinder 3 Exhaust Valve Control Circuit High", + "P3425": "Cylinder 4 Deactivation/lntake Valve Control Circuit/Open", + "P3426": "Cylinder 4 Deactivation/lntake Valve Control Performance", + "P3427": "Cylinder 4 Deactivation/lntake Valve Control Circuit Low", + "P3428": "Cylinder 4 Deactivation/lntake Valve Control Circuit High", + "P3429": "Cylinder 4 Exhaust Valve Control Circuit/Open", + "P3430": "Cylinder 4 Exhaust Valve Control Performance", + "P3431": "Cylinder 4 Exhaust Valve Control Circuit Low", + "P3432": "Cylinder 4 Exhaust Valve Control Circuit High", + "P3433": "Cylinder 5 Deactivation/lntake Valve Control Circuit/Open", + "P3434": "Cylinder 5 Deactivation/lntake Valve Control Performance", + "P3435": "Cylinder 5 Deactivation/lntake Valve Control Circuit Low", + "P3436": "Cylinder 5 Deactivation/lntake Valve Control Circuit High", + "P3437": "Cylinder 5 Exhaust Valve Control Circuit/Open", + "P3438": "Cylinder 5 Exhaust Valve Control Performance", + "P3439": "Cylinder 5 Exhaust Valve Control Circuit Low", + "P3440": "Cylinder 5 Exhaust Valve Control Circuit High", + "P3441": "Cylinder 6 Deactivation/lntake Valve Control Circuit/Open", + "P3442": "Cylinder 6 Deactivation/lntake Valve Control Performance", + "P3443": "Cylinder 6 Deactivation/lntake Valve Control Circuit Low", + "P3444": "Cylinder 6 Deactivation/lntake Valve Control Circuit High", + "P3445": "Cylinder 6 Exhaust Valve Control Circuit/Open", + "P3446": "Cylinder 6 Exhaust Valve Control Performance", + "P3447": "Cylinder 6 Exhaust Valve Control Circuit Low", + "P3448": "Cylinder 6 Exhaust Valve Control Circuit High", + "P3449": "Cylinder 7 Deactivation/lntake Valve Control Circuit/Open", + "P3450": "Cylinder 7 Deactivation/lntake Valve Control Performance", + "P3451": "Cylinder 7 Deactivation/lntake Valve Control Circuit Low", + "P3452": "Cylinder 7 Deactivation/lntake Valve Control Circuit High", + "P3453": "Cylinder 7 Exhaust Valve Control Circuit/Open", + "P3454": "Cylinder 7 Exhaust Valve Control Performance", + "P3455": "Cylinder 7 Exhaust Valve Control Circuit Low", + "P3456": "Cylinder 7 Exhaust Valve Control Circuit High", + "P3457": "Cylinder 8 Deactivation/lntake Valve Control Circuit/Open", + "P3458": "Cylinder 8 Deactivation/lntake Valve Control Performance", + "P3459": "Cylinder 8 Deactivation/lntake Valve Control Circuit Low", + "P3460": "Cylinder 8 Deactivation/lntake Valve Control Circuit High", + "P3461": "Cylinder 8 Exhaust Valve Control Circuit/Open", + "P3462": "Cylinder 8 Exhaust Valve Control Performance", + "P3463": "Cylinder 8 Exhaust Valve Control Circuit Low", + "P3464": "Cylinder 8 Exhaust Valve Control Circuit High", + "P3465": "Cylinder 9 Deactivation/lntake Valve Control Circuit/Open", + "P3466": "Cylinder 9 Deactivation/lntake Valve Control Performance", + "P3467": "Cylinder 9 Deactivation/lntake Valve Control Circuit Low", + "P3468": "Cylinder 9 Deactivation/lntake Valve Control Circuit High", + "P3469": "Cylinder 9 Exhaust Valve Control Circuit/Open", + "P3470": "Cylinder 9 Exhaust Valve Control Performance", + "P3471": "Cylinder 9 Exhaust Valve Control Circuit Low", + "P3472": "Cylinder 9 Exhaust Valve Control Circuit High", + "P3473": "Cylinder 10 Deactivation/lntake Valve Control Circuit/Open", + "P3474": "Cylinder 10 Deactivation/lntake Valve Control Performance", + "P3475": "Cylinder 10 Deactivation/lntake Valve Control Circuit Low", + "P3476": "Cylinder 10 Deactivation/lntake Valve Control Circuit High", + "P3477": "Cylinder 10 Exhaust Valve Control Circuit/Open", + "P3478": "Cylinder 10 Exhaust Valve Control Performance", + "P3479": "Cylinder 10 Exhaust Valve Control Circuit Low", + "P3480": "Cylinder 10 Exhaust Valve Control Circuit High", + "P3481": "Cylinder 11 Deactivation/lntake Valve Control Circuit/Open", + "P3482": "Cylinder 11 Deactivation/lntake Valve Control Performance", + "P3483": "Cylinder 11 Deactivation/lntake Valve Control Circuit Low", + "P3484": "Cylinder 11 Deactivation/lntake Valve Control Circuit High", + "P3485": "Cylinder 11 Exhaust Valve Control Circuit/Open", + "P3486": "Cylinder 11 Exhaust Valve Control Performance", + "P3487": "Cylinder 11 Exhaust Valve Control Circuit Low", + "P3488": "Cylinder 11 Exhaust Valve Control Circuit High", + "P3489": "Cylinder 12 Deactivation/lntake Valve Control Circuit/Open", + "P3490": "Cylinder 12 Deactivation/lntake Valve Control Performance", + "P3491": "Cylinder 12 Deactivation/lntake Valve Control Circuit Low", + "P3492": "Cylinder 12 Deactivation/lntake Valve Control Circuit High", + "P3493": "Cylinder 12 Exhaust Valve Control Circuit/Open", + "P3494": "Cylinder 12 Exhaust Valve Control Performance", + "P3495": "Cylinder 12 Exhaust Valve Control Circuit Low", + "P3496": "Cylinder 12 Exhaust Valve Control Circuit High", + "P3497": "Cylinder Deactivation System", - "U0001" : "High Speed CAN Communication Bus", - "U0002" : "High Speed CAN Communication Bus (Performance)", - "U0003" : "High Speed CAN Communication Bus (Open)", - "U0004" : "High Speed CAN Communication Bus (Low)", - "U0005" : "High Speed CAN Communication Bus (High)", - "U0006" : "High Speed CAN Communication Bus (Open)", - "U0007" : "High Speed CAN Communication Bus (Low)", - "U0008" : "High Speed CAN Communication Bus (High)", - "U0009" : "High Speed CAN Communication Bus (shorted to Bus)", - "U0010" : "Medium Speed CAN Communication Bus", - "U0011" : "Medium Speed CAN Communication Bus (Performance)", - "U0012" : "Medium Speed CAN Communication Bus (Open)", - "U0013" : "Medium Speed CAN Communication Bus (Low)", - "U0014" : "Medium Speed CAN Communication Bus (High)", - "U0015" : "Medium Speed CAN Communication Bus (Open)", - "U0016" : "Medium Speed CAN Communication Bus (Low)", - "U0017" : "Medium Speed CAN Communication Bus (High)", - "U0018" : "Medium Speed CAN Communication Bus (shorted to Bus)", - "U0019" : "Low Speed CAN Communication Bus", - "U0020" : "Low Speed CAN Communication Bus (Performance)", - "U0021" : "Low Speed CAN Communication Bus (Open)", - "U0022" : "Low Speed CAN Communication Bus (Low)", - "U0023" : "Low Speed CAN Communication Bus (High)", - "U0024" : "Low Speed CAN Communication Bus (Open)", - "U0025" : "Low Speed CAN Communication Bus (Low)", - "U0026" : "Low Speed CAN Communication Bus (High)", - "U0027" : "Low Speed CAN Communication Bus (shorted to Bus)", - "U0028" : "Vehicle Communication Bus A", - "U0029" : "Vehicle Communication Bus A (Performance)", - "U0030" : "Vehicle Communication Bus A (Open)", - "U0031" : "Vehicle Communication Bus A (Low)", - "U0032" : "Vehicle Communication Bus A (High)", - "U0033" : "Vehicle Communication Bus A (Open)", - "U0034" : "Vehicle Communication Bus A (Low)", - "U0035" : "Vehicle Communication Bus A (High)", - "U0036" : "Vehicle Communication Bus A (shorted to Bus A)", - "U0037" : "Vehicle Communication Bus B", - "U0038" : "Vehicle Communication Bus B (Performance)", - "U0039" : "Vehicle Communication Bus B (Open)", - "U0040" : "Vehicle Communication Bus B (Low)", - "U0041" : "Vehicle Communication Bus B (High)", - "U0042" : "Vehicle Communication Bus B (Open)", - "U0043" : "Vehicle Communication Bus B (Low)", - "U0044" : "Vehicle Communication Bus B (High)", - "U0045" : "Vehicle Communication Bus B (shorted to Bus B)", - "U0046" : "Vehicle Communication Bus C", - "U0047" : "Vehicle Communication Bus C (Performance)", - "U0048" : "Vehicle Communication Bus C (Open)", - "U0049" : "Vehicle Communication Bus C (Low)", - "U0050" : "Vehicle Communication Bus C (High)", - "U0051" : "Vehicle Communication Bus C (Open)", - "U0052" : "Vehicle Communication Bus C (Low)", - "U0053" : "Vehicle Communication Bus C (High)", - "U0054" : "Vehicle Communication Bus C (shorted to Bus C)", - "U0055" : "Vehicle Communication Bus D", - "U0056" : "Vehicle Communication Bus D (Performance)", - "U0057" : "Vehicle Communication Bus D (Open)", - "U0058" : "Vehicle Communication Bus D (Low)", - "U0059" : "Vehicle Communication Bus D (High)", - "U0060" : "Vehicle Communication Bus D (Open)", - "U0061" : "Vehicle Communication Bus D (Low)", - "U0062" : "Vehicle Communication Bus D (High)", - "U0063" : "Vehicle Communication Bus D (shorted to Bus D)", - "U0064" : "Vehicle Communication Bus E", - "U0065" : "Vehicle Communication Bus E (Performance)", - "U0066" : "Vehicle Communication Bus E (Open)", - "U0067" : "Vehicle Communication Bus E (Low)", - "U0068" : "Vehicle Communication Bus E (High)", - "U0069" : "Vehicle Communication Bus E (Open)", - "U0070" : "Vehicle Communication Bus E (Low)", - "U0071" : "Vehicle Communication Bus E (High)", - "U0072" : "Vehicle Communication Bus E (shorted to Bus E)", - "U0073" : "Control Module Communication Bus Off", - "U0074" : "Reserved by J2012", - "U0075" : "Reserved by J2012", - "U0076" : "Reserved by J2012", - "U0077" : "Reserved by J2012", - "U0078" : "Reserved by J2012", - "U0079" : "Reserved by J2012", - "U0080" : "Reserved by J2012", - "U0081" : "Reserved by J2012", - "U0082" : "Reserved by J2012", - "U0083" : "Reserved by J2012", - "U0084" : "Reserved by J2012", - "U0085" : "Reserved by J2012", - "U0086" : "Reserved by J2012", - "U0087" : "Reserved by J2012", - "U0088" : "Reserved by J2012", - "U0089" : "Reserved by J2012", - "U0090" : "Reserved by J2012", - "U0091" : "Reserved by J2012", - "U0092" : "Reserved by J2012", - "U0093" : "Reserved by J2012", - "U0094" : "Reserved by J2012", - "U0095" : "Reserved by J2012", - "U0096" : "Reserved by J2012", - "U0097" : "Reserved by J2012", - "U0098" : "Reserved by J2012", - "U0099" : "Reserved by J2012", - "U0100" : "Lost Communication With ECM/PCM A", - "U0101" : "Lost Communication with TCM", - "U0102" : "Lost Communication with Transfer Case Control Module", - "U0103" : "Lost Communication With Gear Shift Module", - "U0104" : "Lost Communication With Cruise Control Module", - "U0105" : "Lost Communication With Fuel Injector Control Module", - "U0106" : "Lost Communication With Glow Plug Control Module", - "U0107" : "Lost Communication With Throttle Actuator Control Module", - "U0108" : "Lost Communication With Alternative Fuel Control Module", - "U0109" : "Lost Communication With Fuel Pump Control Module", - "U0110" : "Lost Communication With Drive Motor Control Module", - "U0111" : "Lost Communication With Battery Energy Control Module 'A'", - "U0112" : "Lost Communication With Battery Energy Control Module 'B'", - "U0113" : "Lost Communication With Emissions Critical Control Information", - "U0114" : "Lost Communication With Four-Wheel Drive Clutch Control Module", - "U0115" : "Lost Communication With ECM/PCM B", - "U0116" : "Reserved by J2012", - "U0117" : "Reserved by J2012", - "U0118" : "Reserved by J2012", - "U0119" : "Reserved by J2012", - "U0120" : "Reserved by J2012", - "U0121" : "Lost Communication With Anti-Lock Brake System (ABS) Control Module", - "U0122" : "Lost Communication With Vehicle Dynamics Control Module", - "U0123" : "Lost Communication With Yaw Rate Sensor Module", - "U0124" : "Lost Communication With Lateral Acceleration Sensor Module", - "U0125" : "Lost Communication With Multi-axis Acceleration Sensor Module", - "U0126" : "Lost Communication With Steering Angle Sensor Module", - "U0127" : "Lost Communication With Tire Pressure Monitor Module", - "U0128" : "Lost Communication With Park Brake Control Module", - "U0129" : "Lost Communication With Brake System Control Module", - "U0130" : "Lost Communication With Steering Effort Control Module", - "U0131" : "Lost Communication With Power Steering Control Module", - "U0132" : "Lost Communication With Ride Level Control Module", - "U0133" : "Reserved by J2012", - "U0134" : "Reserved by J2012", - "U0135" : "Reserved by J2012", - "U0136" : "Reserved by J2012", - "U0137" : "Reserved by J2012", - "U0138" : "Reserved by J2012", - "U0139" : "Reserved by J2012", - "U0140" : "Lost Communication With Body Control Module", - "U0141" : "Lost Communication With Body Control Module 'A'", - "U0142" : "Lost Communication With Body Control Module 'B'", - "U0143" : "Lost Communication With Body Control Module 'C'", - "U0144" : "Lost Communication With Body Control Module 'D'", - "U0145" : "Lost Communication With Body Control Module 'E'", - "U0146" : "Lost Communication With Gateway 'A'", - "U0147" : "Lost Communication With Gateway 'B'", - "U0148" : "Lost Communication With Gateway 'C'", - "U0149" : "Lost Communication With Gateway 'D'", - "U0150" : "Lost Communication With Gateway 'E'", - "U0151" : "Lost Communication With Restraints Control Module", - "U0152" : "Lost Communication With Side Restraints Control Module Left", - "U0153" : "Lost Communication With Side Restraints Control Module Right", - "U0154" : "Lost Communication With Restraints Occupant Sensing Control Module", - "U0155" : "Lost Communication With Instrument Panel Cluster (IPC) Control Module", - "U0156" : "Lost Communication With Information Center 'A'", - "U0157" : "Lost Communication With Information Center 'B'", - "U0158" : "Lost Communication With Head Up Display", - "U0159" : "Lost Communication With Parking Assist Control Module", - "U0160" : "Lost Communication With Audible Alert Control Module", - "U0161" : "Lost Communication With Compass Module", - "U0162" : "Lost Communication With Navigation Display Module", - "U0163" : "Lost Communication With Navigation Control Module", - "U0164" : "Lost Communication With HVAC Control Module", - "U0165" : "Lost Communication With HVAC Control Module Rear", - "U0166" : "Lost Communication With Auxiliary Heater Control Module", - "U0167" : "Lost Communication With Vehicle Immobilizer Control Module", - "U0168" : "Lost Communication With Vehicle Security Control Module", - "U0169" : "Lost Communication With Sunroof Control Module", - "U0170" : "Lost Communication With 'Restraints System Sensor A'", - "U0171" : "Lost Communication With 'Restraints System Sensor B'", - "U0172" : "Lost Communication With 'Restraints System Sensor C'", - "U0173" : "Lost Communication With 'Restraints System Sensor D'", - "U0174" : "Lost Communication With 'Restraints System Sensor E'", - "U0175" : "Lost Communication With 'Restraints System Sensor F'", - "U0176" : "Lost Communication With 'Restraints System Sensor G'", - "U0177" : "Lost Communication With 'Restraints System Sensor H'", - "U0178" : "Lost Communication With 'Restraints System Sensor I'", - "U0179" : "Lost Communication With 'Restraints System Sensor J'", - "U0180" : "Lost Communication With Automatic Lighting Control Module", - "U0181" : "Lost Communication With Headlamp Leveling Control Module", - "U0182" : "Lost Communication With Lighting Control Module Front", - "U0183" : "Lost Communication With Lighting Control Module Rear", - "U0184" : "Lost Communication With Radio", - "U0185" : "Lost Communication With Antenna Control Module", - "U0186" : "Lost Communication With Audio Amplifier", - "U0187" : "Lost Communication With Digital Disc Player/Changer Module 'A'", - "U0188" : "Lost Communication With Digital Disc Player/Changer Module 'B'", - "U0189" : "Lost Communication With Digital Disc Player/Changer Module 'C'", - "U0190" : "Lost Communication With Digital Disc Player/Changer Module 'D'", - "U0191" : "Lost Communication With Television", - "U0192" : "Lost Communication With Personal Computer", - "U0193" : "Lost Communication With 'Digital Audio Control Module A'", - "U0194" : "Lost Communication With 'Digital Audio Control Module B'", - "U0195" : "Lost Communication With Subscription Entertainment Receiver Module", - "U0196" : "Lost Communication With Rear Seat Entertainment Control Module", - "U0197" : "Lost Communication With Telephone Control Module", - "U0198" : "Lost Communication With Telematic Control Module", - "U0199" : "Lost Communication With 'Door Control Module A'", - "U0200" : "Lost Communication With 'Door Control Module B'", - "U0201" : "Lost Communication With 'Door Control Module C'", - "U0202" : "Lost Communication With 'Door Control Module D'", - "U0203" : "Lost Communication With 'Door Control Module E'", - "U0204" : "Lost Communication With 'Door Control Module F'", - "U0205" : "Lost Communication With 'Door Control Module G'", - "U0206" : "Lost Communication With Folding Top Control Module", - "U0207" : "Lost Communication With Moveable Roof Control Module", - "U0208" : "Lost Communication With 'Seat Control Module A'", - "U0209" : "Lost Communication With 'Seat Control Module B'", - "U0210" : "Lost Communication With 'Seat Control Module C'", - "U0211" : "Lost Communication With 'Seat Control Module D'", - "U0212" : "Lost Communication With Steering Column Control Module", - "U0213" : "Lost Communication With Mirror Control Module", - "U0214" : "Lost Communication With Remote Function Actuation", - "U0215" : "Lost Communication With 'Door Switch A'", - "U0216" : "Lost Communication With 'Door Switch B'", - "U0217" : "Lost Communication With 'Door Switch C'", - "U0218" : "Lost Communication With 'Door Switch D'", - "U0219" : "Lost Communication With 'Door Switch E'", - "U0220" : "Lost Communication With 'Door Switch F'", - "U0221" : "Lost Communication With 'Door Switch G'", - "U0222" : "Lost Communication With 'Door Window Motor A'", - "U0223" : "Lost Communication With 'Door Window Motor B'", - "U0224" : "Lost Communication With 'Door Window Motor C'", - "U0225" : "Lost Communication With 'Door Window Motor D'", - "U0226" : "Lost Communication With 'Door Window Motor E'", - "U0227" : "Lost Communication With 'Door Window Motor F'", - "U0228" : "Lost Communication With 'Door Window Motor G'", - "U0229" : "Lost Communication With Heated Steering Wheel Module", - "U0230" : "Lost Communication With Rear Gate Module", - "U0231" : "Lost Communication With Rain Sensing Module", - "U0232" : "Lost Communication With Side Obstacle Detection Control Module Left", - "U0233" : "Lost Communication With Side Obstacle Detection Control Module Right", - "U0234" : "Lost Communication With Convenience Recall Module", - "U0235" : "Lost Communication With Cruise Control Front Distance Range Sensor", - "U0300" : "Internal Control Module Software Incompatibility", - "U0301" : "Software Incompatibility with ECM/PCM", - "U0302" : "Software Incompatibility with Transmission Control Module", - "U0303" : "Software Incompatibility with Transfer Case Control Module", - "U0304" : "Software Incompatibility with Gear Shift Control Module", - "U0305" : "Software Incompatibility with Cruise Control Module", - "U0306" : "Software Incompatibility with Fuel Injector Control Module", - "U0307" : "Software Incompatibility with Glow Plug Control Module", - "U0308" : "Software Incompatibility with Throttle Actuator Control Module", - "U0309" : "Software Incompatibility with Alternative Fuel Control Module", - "U0310" : "Software Incompatibility with Fuel Pump Control Module", - "U0311" : "Software Incompatibility with Drive Motor Control Module", - "U0312" : "Software Incompatibility with Battery Energy Control Module A", - "U0313" : "Software Incompatibility with Battery Energy Control Module B", - "U0314" : "Software Incompatibility with Four-Wheel Drive Clutch Control Module", - "U0315" : "Software Incompatibility with Anti-Lock Brake System Control Module", - "U0316" : "Software Incompatibility with Vehicle Dynamics Control Module", - "U0317" : "Software Incompatibility with Park Brake Control Module", - "U0318" : "Software Incompatibility with Brake System Control Module", - "U0319" : "Software Incompatibility with Steering Effort Control Module", - "U0320" : "Software Incompatibility with Power Steering Control Module", - "U0321" : "Software Incompatibility with Ride Level Control Module", - "U0322" : "Software Incompatibility with Body Control Module", - "U0323" : "Software Incompatibility with Instrument Panel Control Module", - "U0324" : "Software Incompatibility with HVAC Control Module", - "U0325" : "Software Incompatibility with Auxiliary Heater Control Module", - "U0326" : "Software Incompatibility with Vehicle Immobilizer Control Module", - "U0327" : "Software Incompatibility with Vehicle Security Control Module", - "U0328" : "Software Incompatibility with Steering Angle Sensor Module", - "U0329" : "Software Incompatibility with Steering Column Control Module", - "U0330" : "Software Incompatibility with Tire Pressure Monitor Module", - "U0331" : "Software Incompatibility with Body Control Module 'A'", - "U0400" : "Invalid Data Received", - "U0401" : "Invalid Data Received From ECM/PCM", - "U0402" : "Invalid Data Received From Transmission Control Module", - "U0403" : "Invalid Data Received From Transfer Case Control Module", - "U0404" : "Invalid Data Received From Gear Shift Control Module", - "U0405" : "Invalid Data Received From Cruise Control Module", - "U0406" : "Invalid Data Received From Fuel Injector Control Module", - "U0407" : "Invalid Data Received From Glow Plug Control Module", - "U0408" : "Invalid Data Received From Throttle Actuator Control Module", - "U0409" : "Invalid Data Received From Alternative Fuel Control Module", - "U0410" : "Invalid Data Received From Fuel Pump Control Module", - "U0411" : "Invalid Data Received From Drive Motor Control Module", - "U0412" : "Invalid Data Received From Battery Energy Control Module A", - "U0413" : "Invalid Data Received From Battery Energy Control Module B", - "U0414" : "Invalid Data Received From Four-Wheel Drive Clutch Control Module", - "U0415" : "Invalid Data Received From Anti-Lock Brake System Control Module", - "U0416" : "Invalid Data Received From Vehicle Dynamics Control Module", - "U0417" : "Invalid Data Received From Park Brake Control Module", - "U0418" : "Invalid Data Received From Brake System Control Module", - "U0419" : "Invalid Data Received From Steering Effort Control Module", - "U0420" : "Invalid Data Received From Power Steering Control Module", - "U0421" : "Invalid Data Received From Ride Level Control Module", - "U0422" : "Invalid Data Received From Body Control Module", - "U0423" : "Invalid Data Received From Instrument Panel Control Module", - "U0424" : "Invalid Data Received From HVAC Control Module", - "U0425" : "Invalid Data Received From Auxiliary Heater Control Module", - "U0426" : "Invalid Data Received From Vehicle Immobilizer Control Module", - "U0427" : "Invalid Data Received From Vehicle Security Control Module", - "U0428" : "Invalid Data Received From Steering Angle Sensor Module", - "U0429" : "Invalid Data Received From Steering Column Control Module", - "U0430" : "Invalid Data Received From Tire Pressure Monitor Module", - "U0431" : "Invalid Data Received From Body Control Module 'A'", + "U0001" : "High Speed CAN Communication Bus", + "U0002" : "High Speed CAN Communication Bus (Performance)", + "U0003" : "High Speed CAN Communication Bus (Open)", + "U0004" : "High Speed CAN Communication Bus (Low)", + "U0005" : "High Speed CAN Communication Bus (High)", + "U0006" : "High Speed CAN Communication Bus (Open)", + "U0007" : "High Speed CAN Communication Bus (Low)", + "U0008" : "High Speed CAN Communication Bus (High)", + "U0009" : "High Speed CAN Communication Bus (shorted to Bus)", + "U0010" : "Medium Speed CAN Communication Bus", + "U0011" : "Medium Speed CAN Communication Bus (Performance)", + "U0012" : "Medium Speed CAN Communication Bus (Open)", + "U0013" : "Medium Speed CAN Communication Bus (Low)", + "U0014" : "Medium Speed CAN Communication Bus (High)", + "U0015" : "Medium Speed CAN Communication Bus (Open)", + "U0016" : "Medium Speed CAN Communication Bus (Low)", + "U0017" : "Medium Speed CAN Communication Bus (High)", + "U0018" : "Medium Speed CAN Communication Bus (shorted to Bus)", + "U0019" : "Low Speed CAN Communication Bus", + "U0020" : "Low Speed CAN Communication Bus (Performance)", + "U0021" : "Low Speed CAN Communication Bus (Open)", + "U0022" : "Low Speed CAN Communication Bus (Low)", + "U0023" : "Low Speed CAN Communication Bus (High)", + "U0024" : "Low Speed CAN Communication Bus (Open)", + "U0025" : "Low Speed CAN Communication Bus (Low)", + "U0026" : "Low Speed CAN Communication Bus (High)", + "U0027" : "Low Speed CAN Communication Bus (shorted to Bus)", + "U0028" : "Vehicle Communication Bus A", + "U0029" : "Vehicle Communication Bus A (Performance)", + "U0030" : "Vehicle Communication Bus A (Open)", + "U0031" : "Vehicle Communication Bus A (Low)", + "U0032" : "Vehicle Communication Bus A (High)", + "U0033" : "Vehicle Communication Bus A (Open)", + "U0034" : "Vehicle Communication Bus A (Low)", + "U0035" : "Vehicle Communication Bus A (High)", + "U0036" : "Vehicle Communication Bus A (shorted to Bus A)", + "U0037" : "Vehicle Communication Bus B", + "U0038" : "Vehicle Communication Bus B (Performance)", + "U0039" : "Vehicle Communication Bus B (Open)", + "U0040" : "Vehicle Communication Bus B (Low)", + "U0041" : "Vehicle Communication Bus B (High)", + "U0042" : "Vehicle Communication Bus B (Open)", + "U0043" : "Vehicle Communication Bus B (Low)", + "U0044" : "Vehicle Communication Bus B (High)", + "U0045" : "Vehicle Communication Bus B (shorted to Bus B)", + "U0046" : "Vehicle Communication Bus C", + "U0047" : "Vehicle Communication Bus C (Performance)", + "U0048" : "Vehicle Communication Bus C (Open)", + "U0049" : "Vehicle Communication Bus C (Low)", + "U0050" : "Vehicle Communication Bus C (High)", + "U0051" : "Vehicle Communication Bus C (Open)", + "U0052" : "Vehicle Communication Bus C (Low)", + "U0053" : "Vehicle Communication Bus C (High)", + "U0054" : "Vehicle Communication Bus C (shorted to Bus C)", + "U0055" : "Vehicle Communication Bus D", + "U0056" : "Vehicle Communication Bus D (Performance)", + "U0057" : "Vehicle Communication Bus D (Open)", + "U0058" : "Vehicle Communication Bus D (Low)", + "U0059" : "Vehicle Communication Bus D (High)", + "U0060" : "Vehicle Communication Bus D (Open)", + "U0061" : "Vehicle Communication Bus D (Low)", + "U0062" : "Vehicle Communication Bus D (High)", + "U0063" : "Vehicle Communication Bus D (shorted to Bus D)", + "U0064" : "Vehicle Communication Bus E", + "U0065" : "Vehicle Communication Bus E (Performance)", + "U0066" : "Vehicle Communication Bus E (Open)", + "U0067" : "Vehicle Communication Bus E (Low)", + "U0068" : "Vehicle Communication Bus E (High)", + "U0069" : "Vehicle Communication Bus E (Open)", + "U0070" : "Vehicle Communication Bus E (Low)", + "U0071" : "Vehicle Communication Bus E (High)", + "U0072" : "Vehicle Communication Bus E (shorted to Bus E)", + "U0073" : "Control Module Communication Bus Off", + "U0074" : "Reserved by J2012", + "U0075" : "Reserved by J2012", + "U0076" : "Reserved by J2012", + "U0077" : "Reserved by J2012", + "U0078" : "Reserved by J2012", + "U0079" : "Reserved by J2012", + "U0080" : "Reserved by J2012", + "U0081" : "Reserved by J2012", + "U0082" : "Reserved by J2012", + "U0083" : "Reserved by J2012", + "U0084" : "Reserved by J2012", + "U0085" : "Reserved by J2012", + "U0086" : "Reserved by J2012", + "U0087" : "Reserved by J2012", + "U0088" : "Reserved by J2012", + "U0089" : "Reserved by J2012", + "U0090" : "Reserved by J2012", + "U0091" : "Reserved by J2012", + "U0092" : "Reserved by J2012", + "U0093" : "Reserved by J2012", + "U0094" : "Reserved by J2012", + "U0095" : "Reserved by J2012", + "U0096" : "Reserved by J2012", + "U0097" : "Reserved by J2012", + "U0098" : "Reserved by J2012", + "U0099" : "Reserved by J2012", + "U0100" : "Lost Communication With ECM/PCM A", + "U0101" : "Lost Communication with TCM", + "U0102" : "Lost Communication with Transfer Case Control Module", + "U0103" : "Lost Communication With Gear Shift Module", + "U0104" : "Lost Communication With Cruise Control Module", + "U0105" : "Lost Communication With Fuel Injector Control Module", + "U0106" : "Lost Communication With Glow Plug Control Module", + "U0107" : "Lost Communication With Throttle Actuator Control Module", + "U0108" : "Lost Communication With Alternative Fuel Control Module", + "U0109" : "Lost Communication With Fuel Pump Control Module", + "U0110" : "Lost Communication With Drive Motor Control Module", + "U0111" : "Lost Communication With Battery Energy Control Module 'A'", + "U0112" : "Lost Communication With Battery Energy Control Module 'B'", + "U0113" : "Lost Communication With Emissions Critical Control Information", + "U0114" : "Lost Communication With Four-Wheel Drive Clutch Control Module", + "U0115" : "Lost Communication With ECM/PCM B", + "U0116" : "Reserved by J2012", + "U0117" : "Reserved by J2012", + "U0118" : "Reserved by J2012", + "U0119" : "Reserved by J2012", + "U0120" : "Reserved by J2012", + "U0121" : "Lost Communication With Anti-Lock Brake System (ABS) Control Module", + "U0122" : "Lost Communication With Vehicle Dynamics Control Module", + "U0123" : "Lost Communication With Yaw Rate Sensor Module", + "U0124" : "Lost Communication With Lateral Acceleration Sensor Module", + "U0125" : "Lost Communication With Multi-axis Acceleration Sensor Module", + "U0126" : "Lost Communication With Steering Angle Sensor Module", + "U0127" : "Lost Communication With Tire Pressure Monitor Module", + "U0128" : "Lost Communication With Park Brake Control Module", + "U0129" : "Lost Communication With Brake System Control Module", + "U0130" : "Lost Communication With Steering Effort Control Module", + "U0131" : "Lost Communication With Power Steering Control Module", + "U0132" : "Lost Communication With Ride Level Control Module", + "U0133" : "Reserved by J2012", + "U0134" : "Reserved by J2012", + "U0135" : "Reserved by J2012", + "U0136" : "Reserved by J2012", + "U0137" : "Reserved by J2012", + "U0138" : "Reserved by J2012", + "U0139" : "Reserved by J2012", + "U0140" : "Lost Communication With Body Control Module", + "U0141" : "Lost Communication With Body Control Module 'A'", + "U0142" : "Lost Communication With Body Control Module 'B'", + "U0143" : "Lost Communication With Body Control Module 'C'", + "U0144" : "Lost Communication With Body Control Module 'D'", + "U0145" : "Lost Communication With Body Control Module 'E'", + "U0146" : "Lost Communication With Gateway 'A'", + "U0147" : "Lost Communication With Gateway 'B'", + "U0148" : "Lost Communication With Gateway 'C'", + "U0149" : "Lost Communication With Gateway 'D'", + "U0150" : "Lost Communication With Gateway 'E'", + "U0151" : "Lost Communication With Restraints Control Module", + "U0152" : "Lost Communication With Side Restraints Control Module Left", + "U0153" : "Lost Communication With Side Restraints Control Module Right", + "U0154" : "Lost Communication With Restraints Occupant Sensing Control Module", + "U0155" : "Lost Communication With Instrument Panel Cluster (IPC) Control Module", + "U0156" : "Lost Communication With Information Center 'A'", + "U0157" : "Lost Communication With Information Center 'B'", + "U0158" : "Lost Communication With Head Up Display", + "U0159" : "Lost Communication With Parking Assist Control Module", + "U0160" : "Lost Communication With Audible Alert Control Module", + "U0161" : "Lost Communication With Compass Module", + "U0162" : "Lost Communication With Navigation Display Module", + "U0163" : "Lost Communication With Navigation Control Module", + "U0164" : "Lost Communication With HVAC Control Module", + "U0165" : "Lost Communication With HVAC Control Module Rear", + "U0166" : "Lost Communication With Auxiliary Heater Control Module", + "U0167" : "Lost Communication With Vehicle Immobilizer Control Module", + "U0168" : "Lost Communication With Vehicle Security Control Module", + "U0169" : "Lost Communication With Sunroof Control Module", + "U0170" : "Lost Communication With 'Restraints System Sensor A'", + "U0171" : "Lost Communication With 'Restraints System Sensor B'", + "U0172" : "Lost Communication With 'Restraints System Sensor C'", + "U0173" : "Lost Communication With 'Restraints System Sensor D'", + "U0174" : "Lost Communication With 'Restraints System Sensor E'", + "U0175" : "Lost Communication With 'Restraints System Sensor F'", + "U0176" : "Lost Communication With 'Restraints System Sensor G'", + "U0177" : "Lost Communication With 'Restraints System Sensor H'", + "U0178" : "Lost Communication With 'Restraints System Sensor I'", + "U0179" : "Lost Communication With 'Restraints System Sensor J'", + "U0180" : "Lost Communication With Automatic Lighting Control Module", + "U0181" : "Lost Communication With Headlamp Leveling Control Module", + "U0182" : "Lost Communication With Lighting Control Module Front", + "U0183" : "Lost Communication With Lighting Control Module Rear", + "U0184" : "Lost Communication With Radio", + "U0185" : "Lost Communication With Antenna Control Module", + "U0186" : "Lost Communication With Audio Amplifier", + "U0187" : "Lost Communication With Digital Disc Player/Changer Module 'A'", + "U0188" : "Lost Communication With Digital Disc Player/Changer Module 'B'", + "U0189" : "Lost Communication With Digital Disc Player/Changer Module 'C'", + "U0190" : "Lost Communication With Digital Disc Player/Changer Module 'D'", + "U0191" : "Lost Communication With Television", + "U0192" : "Lost Communication With Personal Computer", + "U0193" : "Lost Communication With 'Digital Audio Control Module A'", + "U0194" : "Lost Communication With 'Digital Audio Control Module B'", + "U0195" : "Lost Communication With Subscription Entertainment Receiver Module", + "U0196" : "Lost Communication With Rear Seat Entertainment Control Module", + "U0197" : "Lost Communication With Telephone Control Module", + "U0198" : "Lost Communication With Telematic Control Module", + "U0199" : "Lost Communication With 'Door Control Module A'", + "U0200" : "Lost Communication With 'Door Control Module B'", + "U0201" : "Lost Communication With 'Door Control Module C'", + "U0202" : "Lost Communication With 'Door Control Module D'", + "U0203" : "Lost Communication With 'Door Control Module E'", + "U0204" : "Lost Communication With 'Door Control Module F'", + "U0205" : "Lost Communication With 'Door Control Module G'", + "U0206" : "Lost Communication With Folding Top Control Module", + "U0207" : "Lost Communication With Moveable Roof Control Module", + "U0208" : "Lost Communication With 'Seat Control Module A'", + "U0209" : "Lost Communication With 'Seat Control Module B'", + "U0210" : "Lost Communication With 'Seat Control Module C'", + "U0211" : "Lost Communication With 'Seat Control Module D'", + "U0212" : "Lost Communication With Steering Column Control Module", + "U0213" : "Lost Communication With Mirror Control Module", + "U0214" : "Lost Communication With Remote Function Actuation", + "U0215" : "Lost Communication With 'Door Switch A'", + "U0216" : "Lost Communication With 'Door Switch B'", + "U0217" : "Lost Communication With 'Door Switch C'", + "U0218" : "Lost Communication With 'Door Switch D'", + "U0219" : "Lost Communication With 'Door Switch E'", + "U0220" : "Lost Communication With 'Door Switch F'", + "U0221" : "Lost Communication With 'Door Switch G'", + "U0222" : "Lost Communication With 'Door Window Motor A'", + "U0223" : "Lost Communication With 'Door Window Motor B'", + "U0224" : "Lost Communication With 'Door Window Motor C'", + "U0225" : "Lost Communication With 'Door Window Motor D'", + "U0226" : "Lost Communication With 'Door Window Motor E'", + "U0227" : "Lost Communication With 'Door Window Motor F'", + "U0228" : "Lost Communication With 'Door Window Motor G'", + "U0229" : "Lost Communication With Heated Steering Wheel Module", + "U0230" : "Lost Communication With Rear Gate Module", + "U0231" : "Lost Communication With Rain Sensing Module", + "U0232" : "Lost Communication With Side Obstacle Detection Control Module Left", + "U0233" : "Lost Communication With Side Obstacle Detection Control Module Right", + "U0234" : "Lost Communication With Convenience Recall Module", + "U0235" : "Lost Communication With Cruise Control Front Distance Range Sensor", + "U0300" : "Internal Control Module Software Incompatibility", + "U0301" : "Software Incompatibility with ECM/PCM", + "U0302" : "Software Incompatibility with Transmission Control Module", + "U0303" : "Software Incompatibility with Transfer Case Control Module", + "U0304" : "Software Incompatibility with Gear Shift Control Module", + "U0305" : "Software Incompatibility with Cruise Control Module", + "U0306" : "Software Incompatibility with Fuel Injector Control Module", + "U0307" : "Software Incompatibility with Glow Plug Control Module", + "U0308" : "Software Incompatibility with Throttle Actuator Control Module", + "U0309" : "Software Incompatibility with Alternative Fuel Control Module", + "U0310" : "Software Incompatibility with Fuel Pump Control Module", + "U0311" : "Software Incompatibility with Drive Motor Control Module", + "U0312" : "Software Incompatibility with Battery Energy Control Module A", + "U0313" : "Software Incompatibility with Battery Energy Control Module B", + "U0314" : "Software Incompatibility with Four-Wheel Drive Clutch Control Module", + "U0315" : "Software Incompatibility with Anti-Lock Brake System Control Module", + "U0316" : "Software Incompatibility with Vehicle Dynamics Control Module", + "U0317" : "Software Incompatibility with Park Brake Control Module", + "U0318" : "Software Incompatibility with Brake System Control Module", + "U0319" : "Software Incompatibility with Steering Effort Control Module", + "U0320" : "Software Incompatibility with Power Steering Control Module", + "U0321" : "Software Incompatibility with Ride Level Control Module", + "U0322" : "Software Incompatibility with Body Control Module", + "U0323" : "Software Incompatibility with Instrument Panel Control Module", + "U0324" : "Software Incompatibility with HVAC Control Module", + "U0325" : "Software Incompatibility with Auxiliary Heater Control Module", + "U0326" : "Software Incompatibility with Vehicle Immobilizer Control Module", + "U0327" : "Software Incompatibility with Vehicle Security Control Module", + "U0328" : "Software Incompatibility with Steering Angle Sensor Module", + "U0329" : "Software Incompatibility with Steering Column Control Module", + "U0330" : "Software Incompatibility with Tire Pressure Monitor Module", + "U0331" : "Software Incompatibility with Body Control Module 'A'", + "U0400" : "Invalid Data Received", + "U0401" : "Invalid Data Received From ECM/PCM", + "U0402" : "Invalid Data Received From Transmission Control Module", + "U0403" : "Invalid Data Received From Transfer Case Control Module", + "U0404" : "Invalid Data Received From Gear Shift Control Module", + "U0405" : "Invalid Data Received From Cruise Control Module", + "U0406" : "Invalid Data Received From Fuel Injector Control Module", + "U0407" : "Invalid Data Received From Glow Plug Control Module", + "U0408" : "Invalid Data Received From Throttle Actuator Control Module", + "U0409" : "Invalid Data Received From Alternative Fuel Control Module", + "U0410" : "Invalid Data Received From Fuel Pump Control Module", + "U0411" : "Invalid Data Received From Drive Motor Control Module", + "U0412" : "Invalid Data Received From Battery Energy Control Module A", + "U0413" : "Invalid Data Received From Battery Energy Control Module B", + "U0414" : "Invalid Data Received From Four-Wheel Drive Clutch Control Module", + "U0415" : "Invalid Data Received From Anti-Lock Brake System Control Module", + "U0416" : "Invalid Data Received From Vehicle Dynamics Control Module", + "U0417" : "Invalid Data Received From Park Brake Control Module", + "U0418" : "Invalid Data Received From Brake System Control Module", + "U0419" : "Invalid Data Received From Steering Effort Control Module", + "U0420" : "Invalid Data Received From Power Steering Control Module", + "U0421" : "Invalid Data Received From Ride Level Control Module", + "U0422" : "Invalid Data Received From Body Control Module", + "U0423" : "Invalid Data Received From Instrument Panel Control Module", + "U0424" : "Invalid Data Received From HVAC Control Module", + "U0425" : "Invalid Data Received From Auxiliary Heater Control Module", + "U0426" : "Invalid Data Received From Vehicle Immobilizer Control Module", + "U0427" : "Invalid Data Received From Vehicle Security Control Module", + "U0428" : "Invalid Data Received From Steering Angle Sensor Module", + "U0429" : "Invalid Data Received From Steering Column Control Module", + "U0430" : "Invalid Data Received From Tire Pressure Monitor Module", + "U0431" : "Invalid Data Received From Body Control Module 'A'", } IGNITION_TYPE = [ - "Spark", - "Compression", + "Spark", + "Compression", ] SPARK_TESTS = [ - "EGR System", - "Oxygen Sensor Heater", - "Oxygen Sensor", - "A/C Refrigerant", - "Secondary Air System", - "Evaporative System", - "Heated Catalyst", - "Catalyst", + "EGR System", + "Oxygen Sensor Heater", + "Oxygen Sensor", + "A/C Refrigerant", + "Secondary Air System", + "Evaporative System", + "Heated Catalyst", + "Catalyst", ] COMPRESSION_TESTS = [ - "EGR and/or VVT System", - "PM filter monitoring", - "Exhaust Gas Sensor", - "None", - "Boost Pressure", - "None", - "NOx/SCR Monitor", - "NMHC Catalyst", + "EGR and/or VVT System", + "PM filter monitoring", + "Exhaust Gas Sensor", + "None", + "Boost Pressure", + "None", + "NOx/SCR Monitor", + "NMHC Catalyst", ] FUEL_STATUS = [ - "Open loop due to insufficient engine temperature", - "Closed loop, using oxygen sensor feedback to determine fuel mix", - "Open loop due to engine load OR fuel cut due to deceleration", - "Open loop due to system failure", - "Closed loop, using at least one oxygen sensor but there is a fault in the feedback system", + "Open loop due to insufficient engine temperature", + "Closed loop, using oxygen sensor feedback to determine fuel mix", + "Open loop due to engine load OR fuel cut due to deceleration", + "Open loop due to system failure", + "Closed loop, using at least one oxygen sensor but there is a fault in the feedback system", ] AIR_STATUS = [ - "Upstream", - "Downstream of catalytic converter", - "From the outside atmosphere or off", - "Pump commanded on for diagnostics", + "Upstream", + "Downstream of catalytic converter", + "From the outside atmosphere or off", + "Pump commanded on for diagnostics", ] OBD_COMPLIANCE = [ - "Undefined", - "OBD-II as defined by the CARB", - "OBD as defined by the EPA", - "OBD and OBD-II", - "OBD-I", - "Not OBD compliant", - "EOBD (Europe)", - "EOBD and OBD-II", - "EOBD and OBD", - "EOBD, OBD and OBD II", - "JOBD (Japan)", - "JOBD and OBD II", - "JOBD and EOBD", - "JOBD, EOBD, and OBD II", - "Reserved", - "Reserved", - "Reserved", - "Engine Manufacturer Diagnostics (EMD)", - "Engine Manufacturer Diagnostics Enhanced (EMD+)", - "Heavy Duty On-Board Diagnostics (Child/Partial) (HD OBD-C)", - "Heavy Duty On-Board Diagnostics (HD OBD)", - "World Wide Harmonized OBD (WWH OBD)", - "Reserved", - "Heavy Duty Euro OBD Stage I without NOx control (HD EOBD-I)", - "Heavy Duty Euro OBD Stage I with NOx control (HD EOBD-I N)", - "Heavy Duty Euro OBD Stage II without NOx control (HD EOBD-II)", - "Heavy Duty Euro OBD Stage II with NOx control (HD EOBD-II N)", - "Reserved", - "Brazil OBD Phase 1 (OBDBr-1)", - "Brazil OBD Phase 2 (OBDBr-2)", - "Korean OBD (KOBD)", - "India OBD I (IOBD I)", - "India OBD II (IOBD II)", - "Heavy Duty Euro OBD Stage VI (HD EOBD-IV)", + "Undefined", + "OBD-II as defined by the CARB", + "OBD as defined by the EPA", + "OBD and OBD-II", + "OBD-I", + "Not OBD compliant", + "EOBD (Europe)", + "EOBD and OBD-II", + "EOBD and OBD", + "EOBD, OBD and OBD II", + "JOBD (Japan)", + "JOBD and OBD II", + "JOBD and EOBD", + "JOBD, EOBD, and OBD II", + "Reserved", + "Reserved", + "Reserved", + "Engine Manufacturer Diagnostics (EMD)", + "Engine Manufacturer Diagnostics Enhanced (EMD+)", + "Heavy Duty On-Board Diagnostics (Child/Partial) (HD OBD-C)", + "Heavy Duty On-Board Diagnostics (HD OBD)", + "World Wide Harmonized OBD (WWH OBD)", + "Reserved", + "Heavy Duty Euro OBD Stage I without NOx control (HD EOBD-I)", + "Heavy Duty Euro OBD Stage I with NOx control (HD EOBD-I N)", + "Heavy Duty Euro OBD Stage II without NOx control (HD EOBD-II)", + "Heavy Duty Euro OBD Stage II with NOx control (HD EOBD-II N)", + "Reserved", + "Brazil OBD Phase 1 (OBDBr-1)", + "Brazil OBD Phase 2 (OBDBr-2)", + "Korean OBD (KOBD)", + "India OBD I (IOBD I)", + "India OBD II (IOBD II)", + "Heavy Duty Euro OBD Stage VI (HD EOBD-IV)", ] FUEL_TYPES = [ - "Not available", - "Gasoline", - "Methanol", - "Ethanol", - "Diesel", - "LPG", - "CNG", - "Propane", - "Electric", - "Bifuel running Gasoline", - "Bifuel running Methanol", - "Bifuel running Ethanol", - "Bifuel running LPG", - "Bifuel running CNG", - "Bifuel running Propane", - "Bifuel running Electricity", - "Bifuel running electric and combustion engine", - "Hybrid gasoline", - "Hybrid Ethanol", - "Hybrid Diesel", - "Hybrid Electric", - "Hybrid running electric and combustion engine", - "Hybrid Regenerative", - "Bifuel running diesel", + "Not available", + "Gasoline", + "Methanol", + "Ethanol", + "Diesel", + "LPG", + "CNG", + "Propane", + "Electric", + "Bifuel running Gasoline", + "Bifuel running Methanol", + "Bifuel running Ethanol", + "Bifuel running LPG", + "Bifuel running CNG", + "Bifuel running Propane", + "Bifuel running Electricity", + "Bifuel running electric and combustion engine", + "Hybrid gasoline", + "Hybrid Ethanol", + "Hybrid Diesel", + "Hybrid Electric", + "Hybrid running electric and combustion engine", + "Hybrid Regenerative", + "Bifuel running diesel", ] diff --git a/obd/commands.py b/obd/commands.py index 493a3675..f4e39b91 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -44,133 +44,133 @@ # NOTE: commands MUST be in PID order, one command per PID (for fast lookup using __mode1__[pid]) __mode1__ = [ - # sensor name description mode cmd bytes decoder - OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "01", "00", 4, pid , True), # the first PID getter is assumed to be supported - OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01", 4, status ), - OBDCommand("FREEZE_DTC" , "Freeze DTC" , "01", "02", 2, noop ), - OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03", 2, fuel_status ), - OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , "01", "04", 1, percent ), - OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05", 1, temp ), - OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06", 1, percent_centered ), - OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07", 1, percent_centered ), - OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08", 1, percent_centered ), - OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09", 1, percent_centered ), - OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A", 1, fuel_pressure ), - OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "01", "0B", 1, pressure ), - OBDCommand("RPM" , "Engine RPM" , "01", "0C", 2, rpm ), - OBDCommand("SPEED" , "Vehicle Speed" , "01", "0D", 1, speed ), - OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E", 1, timing_advance ), - OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F", 1, temp ), - OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10", 2, maf ), - OBDCommand("THROTTLE_POS" , "Throttle Position" , "01", "11", 1, percent ), - OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12", 1, air_status ), - OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13", 1, noop ), - OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , "01", "14", 2, sensor_voltage ), - OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , "01", "15", 2, sensor_voltage ), - OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , "01", "16", 2, sensor_voltage ), - OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , "01", "17", 2, sensor_voltage ), - OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , "01", "18", 2, sensor_voltage ), - OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , "01", "19", 2, sensor_voltage ), - OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , "01", "1A", 2, sensor_voltage ), - OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , "01", "1B", 2, sensor_voltage ), - OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "01", "1C", 1, obd_compliance ), - OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D", 1, noop ), - OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E", 1, noop ), - OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F", 2, seconds ), - - # sensor name description mode cmd bytes decoder - OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "01", "20", 4, pid ), - OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , "01", "21", 2, distance ), - OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , "01", "22", 2, fuel_pres_vac ), - OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , "01", "23", 2, fuel_pres_direct ), - OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , "01", "24", 4, sensor_voltage_big ), - OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , "01", "25", 4, sensor_voltage_big ), - OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , "01", "26", 4, sensor_voltage_big ), - OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , "01", "27", 4, sensor_voltage_big ), - OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , "01", "28", 4, sensor_voltage_big ), - OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , "01", "29", 4, sensor_voltage_big ), - OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , "01", "2A", 4, sensor_voltage_big ), - OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , "01", "2B", 4, sensor_voltage_big ), - OBDCommand("COMMANDED_EGR" , "Commanded EGR" , "01", "2C", 1, percent ), - OBDCommand("EGR_ERROR" , "EGR Error" , "01", "2D", 1, percent_centered ), - OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , "01", "2E", 1, percent ), - OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , "01", "2F", 1, percent ), - OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , "01", "30", 1, count ), - OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , "01", "31", 2, distance ), - OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , "01", "32", 2, evap_pressure ), - OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , "01", "33", 1, pressure ), - OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , "01", "34", 4, current_centered ), - OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , "01", "35", 4, current_centered ), - OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , "01", "36", 4, current_centered ), - OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , "01", "37", 4, current_centered ), - OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , "01", "38", 4, current_centered ), - OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , "01", "39", 4, current_centered ), - OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , "01", "3A", 4, current_centered ), - OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , "01", "3B", 4, current_centered ), - OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , "01", "3C", 2, catalyst_temp ), - OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , "01", "3D", 2, catalyst_temp ), - OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , "01", "3E", 2, catalyst_temp ), - OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , "01", "3F", 2, catalyst_temp ), - - # sensor name description mode cmd bytes decoder - OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "01", "40", 4, pid ), - OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "01", "41", 4, todo ), - OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "01", "42", 2, todo ), - OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "01", "43", 2, todo ), - OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "01", "44", 2, todo ), - OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , "01", "45", 1, percent ), - OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , "01", "46", 1, temp ), - OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , "01", "47", 1, percent ), - OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , "01", "48", 1, percent ), - OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , "01", "49", 1, percent ), - OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , "01", "4A", 1, percent ), - OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , "01", "4B", 1, percent ), - OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , "01", "4C", 1, percent ), - OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , "01", "4D", 2, minutes ), - OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , "01", "4E", 2, minutes ), - OBDCommand("MAX_VALUES" , "Various Max values" , "01", "4F", 4, noop ), # todo: decode this - OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , "01", "50", 4, max_maf ), - OBDCommand("FUEL_TYPE" , "Fuel Type" , "01", "51", 1, fuel_type ), - OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , "01", "52", 1, percent ), - OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , "01", "53", 2, abs_evap_pressure ), - OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , "01", "54", 2, evap_pressure_alt ), - OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , "01", "55", 2, percent_centered ), # todo: decode seconds value for banks 3 and 4 - OBDCommand("LONG_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , "01", "56", 2, percent_centered ), - OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , "01", "57", 2, percent_centered ), - OBDCommand("LONG_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , "01", "58", 2, percent_centered ), - OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , "01", "59", 2, fuel_pres_direct ), - OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , "01", "5A", 1, percent ), - OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , "01", "5B", 1, percent ), - OBDCommand("OIL_TEMP" , "Engine oil temperature" , "01", "5C", 1, temp ), - OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , "01", "5D", 2, inject_timing ), - OBDCommand("FUEL_RATE" , "Engine fuel rate" , "01", "5E", 2, fuel_rate ), - OBDCommand("EMISSION_REQ" , "Designed emission requirements" , "01", "5F", 1, noop ), + # sensor name description mode cmd bytes decoder + OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "01", "00", 4, pid , True), # the first PID getter is assumed to be supported + OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01", 4, status ), + OBDCommand("FREEZE_DTC" , "Freeze DTC" , "01", "02", 2, noop ), + OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03", 2, fuel_status ), + OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , "01", "04", 1, percent ), + OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05", 1, temp ), + OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06", 1, percent_centered ), + OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07", 1, percent_centered ), + OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08", 1, percent_centered ), + OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09", 1, percent_centered ), + OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A", 1, fuel_pressure ), + OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "01", "0B", 1, pressure ), + OBDCommand("RPM" , "Engine RPM" , "01", "0C", 2, rpm ), + OBDCommand("SPEED" , "Vehicle Speed" , "01", "0D", 1, speed ), + OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E", 1, timing_advance ), + OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F", 1, temp ), + OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10", 2, maf ), + OBDCommand("THROTTLE_POS" , "Throttle Position" , "01", "11", 1, percent ), + OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12", 1, air_status ), + OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13", 1, noop ), + OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , "01", "14", 2, sensor_voltage ), + OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , "01", "15", 2, sensor_voltage ), + OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , "01", "16", 2, sensor_voltage ), + OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , "01", "17", 2, sensor_voltage ), + OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , "01", "18", 2, sensor_voltage ), + OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , "01", "19", 2, sensor_voltage ), + OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , "01", "1A", 2, sensor_voltage ), + OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , "01", "1B", 2, sensor_voltage ), + OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "01", "1C", 1, obd_compliance ), + OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D", 1, noop ), + OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E", 1, noop ), + OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F", 2, seconds ), + + # sensor name description mode cmd bytes decoder + OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "01", "20", 4, pid ), + OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , "01", "21", 2, distance ), + OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , "01", "22", 2, fuel_pres_vac ), + OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , "01", "23", 2, fuel_pres_direct ), + OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , "01", "24", 4, sensor_voltage_big ), + OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , "01", "25", 4, sensor_voltage_big ), + OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , "01", "26", 4, sensor_voltage_big ), + OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , "01", "27", 4, sensor_voltage_big ), + OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , "01", "28", 4, sensor_voltage_big ), + OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , "01", "29", 4, sensor_voltage_big ), + OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , "01", "2A", 4, sensor_voltage_big ), + OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , "01", "2B", 4, sensor_voltage_big ), + OBDCommand("COMMANDED_EGR" , "Commanded EGR" , "01", "2C", 1, percent ), + OBDCommand("EGR_ERROR" , "EGR Error" , "01", "2D", 1, percent_centered ), + OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , "01", "2E", 1, percent ), + OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , "01", "2F", 1, percent ), + OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , "01", "30", 1, count ), + OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , "01", "31", 2, distance ), + OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , "01", "32", 2, evap_pressure ), + OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , "01", "33", 1, pressure ), + OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , "01", "34", 4, current_centered ), + OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , "01", "35", 4, current_centered ), + OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , "01", "36", 4, current_centered ), + OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , "01", "37", 4, current_centered ), + OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , "01", "38", 4, current_centered ), + OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , "01", "39", 4, current_centered ), + OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , "01", "3A", 4, current_centered ), + OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , "01", "3B", 4, current_centered ), + OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , "01", "3C", 2, catalyst_temp ), + OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , "01", "3D", 2, catalyst_temp ), + OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , "01", "3E", 2, catalyst_temp ), + OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , "01", "3F", 2, catalyst_temp ), + + # sensor name description mode cmd bytes decoder + OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "01", "40", 4, pid ), + OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "01", "41", 4, todo ), + OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "01", "42", 2, todo ), + OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "01", "43", 2, todo ), + OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "01", "44", 2, todo ), + OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , "01", "45", 1, percent ), + OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , "01", "46", 1, temp ), + OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , "01", "47", 1, percent ), + OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , "01", "48", 1, percent ), + OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , "01", "49", 1, percent ), + OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , "01", "4A", 1, percent ), + OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , "01", "4B", 1, percent ), + OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , "01", "4C", 1, percent ), + OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , "01", "4D", 2, minutes ), + OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , "01", "4E", 2, minutes ), + OBDCommand("MAX_VALUES" , "Various Max values" , "01", "4F", 4, noop ), # todo: decode this + OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , "01", "50", 4, max_maf ), + OBDCommand("FUEL_TYPE" , "Fuel Type" , "01", "51", 1, fuel_type ), + OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , "01", "52", 1, percent ), + OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , "01", "53", 2, abs_evap_pressure ), + OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , "01", "54", 2, evap_pressure_alt ), + OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , "01", "55", 2, percent_centered ), # todo: decode seconds value for banks 3 and 4 + OBDCommand("LONG_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , "01", "56", 2, percent_centered ), + OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , "01", "57", 2, percent_centered ), + OBDCommand("LONG_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , "01", "58", 2, percent_centered ), + OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , "01", "59", 2, fuel_pres_direct ), + OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , "01", "5A", 1, percent ), + OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , "01", "5B", 1, percent ), + OBDCommand("OIL_TEMP" , "Engine oil temperature" , "01", "5C", 1, temp ), + OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , "01", "5D", 2, inject_timing ), + OBDCommand("FUEL_RATE" , "Engine fuel rate" , "01", "5E", 2, fuel_rate ), + OBDCommand("EMISSION_REQ" , "Designed emission requirements" , "01", "5F", 1, noop ), ] # mode 2 is the same as mode 1, but returns values from when the DTC occured __mode2__ = [] for c in __mode1__: - c = c.clone() - c.mode = "02" - c.name = "DTC_" + c.name - c.desc = "DTC " + c.desc - __mode2__.append(c) + c = c.clone() + c.mode = "02" + c.name = "DTC_" + c.name + c.desc = "DTC " + c.desc + __mode2__.append(c) __mode3__ = [ - # sensor name description mode cmd bytes decoder - OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 6, dtc , True), + # sensor name description mode cmd bytes decoder + OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 6, dtc , True), ] __mode4__ = [ - # sensor name description mode cmd bytes decoder - OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", "" , 0, noop , True), + # sensor name description mode cmd bytes decoder + OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", "" , 0, noop , True), ] __mode7__ = [ - # sensor name description mode cmd bytes decoder - OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", "" , 0, dtc , True), + # sensor name description mode cmd bytes decoder + OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", "" , 0, dtc , True), ] @@ -180,96 +180,96 @@ ''' class Commands(): - def __init__(self): - - # allow commands to be accessed by mode and PID - self.modes = [ - [], - __mode1__, - __mode2__, - __mode3__, - __mode4__, - [], - [], - __mode7__ - ] - - # allow commands to be accessed by sensor name - for m in self.modes: - for c in m: - self.__dict__[c.name] = c - - - def __getitem__(self, key): - if isinstance(key, int): - return self.modes[key] - elif isinstance(key, str): - return self.__dict__[key] - else: - debug("OBD commands can only be retrieved by PID value or dict name", True) - - - def __len__(self): - l = 0 - for m in self.modes: - l += len(m) - return l - - - def __contains__(self, s): - return self.has_name(s) - - - # returns a list of PID GET commands - def pid_getters(self): - getters = [] - for m in self.modes: - for c in m: - if c.decode == pid: # GET commands have a special decoder - getters.append(c) - return getters - - - # sets the boolean supported flag for the given command - def set_supported(self, mode, pid, v): - if isinstance(v, bool): - if self.has(mode, pid): - self.modes[mode][pid].supported = v - else: - debug("set_supported() only accepts boolean values", True) - - - # checks for existance of command by OBDCommand object - def has_command(self, c): - if isinstance(c, OBDCommand): - return c in self.__dict__.values() - else: - debug("has_command() only accepts OBDCommand objects", True) - return False - - - # checks for existance of command by name - def has_name(self, s): - if isinstance(s, str): - return s.isupper() and (s in self.__dict__.keys()) - else: - debug("has_name() only accepts string names for commands", True) - return False - - - # checks for existance of int mode and int pid - def has_pid(self, mode, pid): - if isinstance(mode, int) and isinstance(pid, int): - if (mode < 0) or (pid < 0): - return False - if mode >= len(self.modes): - return False - if pid >= len(self.modes[mode]): - return False - return True - else: - debug("has_pid() only accepts integer values for mode and PID", True) - return False + def __init__(self): + + # allow commands to be accessed by mode and PID + self.modes = [ + [], + __mode1__, + __mode2__, + __mode3__, + __mode4__, + [], + [], + __mode7__ + ] + + # allow commands to be accessed by sensor name + for m in self.modes: + for c in m: + self.__dict__[c.name] = c + + + def __getitem__(self, key): + if isinstance(key, int): + return self.modes[key] + elif isinstance(key, str): + return self.__dict__[key] + else: + debug("OBD commands can only be retrieved by PID value or dict name", True) + + + def __len__(self): + l = 0 + for m in self.modes: + l += len(m) + return l + + + def __contains__(self, s): + return self.has_name(s) + + + # returns a list of PID GET commands + def pid_getters(self): + getters = [] + for m in self.modes: + for c in m: + if c.decode == pid: # GET commands have a special decoder + getters.append(c) + return getters + + + # sets the boolean supported flag for the given command + def set_supported(self, mode, pid, v): + if isinstance(v, bool): + if self.has(mode, pid): + self.modes[mode][pid].supported = v + else: + debug("set_supported() only accepts boolean values", True) + + + # checks for existance of command by OBDCommand object + def has_command(self, c): + if isinstance(c, OBDCommand): + return c in self.__dict__.values() + else: + debug("has_command() only accepts OBDCommand objects", True) + return False + + + # checks for existance of command by name + def has_name(self, s): + if isinstance(s, str): + return s.isupper() and (s in self.__dict__.keys()) + else: + debug("has_name() only accepts string names for commands", True) + return False + + + # checks for existance of int mode and int pid + def has_pid(self, mode, pid): + if isinstance(mode, int) and isinstance(pid, int): + if (mode < 0) or (pid < 0): + return False + if mode >= len(self.modes): + return False + if pid >= len(self.modes[mode]): + return False + return True + else: + debug("has_pid() only accepts integer values for mode and PID", True) + return False # export this object diff --git a/obd/debug.py b/obd/debug.py index 0bd9c696..336ae418 100644 --- a/obd/debug.py +++ b/obd/debug.py @@ -30,21 +30,21 @@ ######################################################################## class Debug(): - def __init__(self): - self.console = False - self.handler = None + def __init__(self): + self.console = False + self.handler = None - def __call__(self, msg, forcePrint=False): + def __call__(self, msg, forcePrint=False): - if self.console or forcePrint: - print("[obd] " + str(msg)) + if self.console or forcePrint: + print("[obd] " + str(msg)) - if hasattr(self.handler, '__call__'): - self.handler(msg) + if hasattr(self.handler, '__call__'): + self.handler(msg) debug = Debug() class ProtocolError(Exception): - def __init__(self): - pass + def __init__(self): + pass diff --git a/obd/decoders.py b/obd/decoders.py index 4c299b1a..a04a44c1 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -38,24 +38,24 @@ All decoders take the form: def (_hex): - ... - return (, ) + ... + return (, ) ''' # todo def todo(_hex): - return (_hex, Unit.NONE) + return (_hex, Unit.NONE) # hex in, hex out def noop(_hex): - return (_hex, Unit.NONE) + return (_hex, Unit.NONE) # hex in, bitstring out def pid(_hex): - v = bitstring(_hex, len(_hex) * 4) - return (v, Unit.NONE) + v = bitstring(_hex, len(_hex) * 4) + return (v, Unit.NONE) ''' Sensor decoders @@ -63,149 +63,149 @@ def pid(_hex): ''' def count(_hex): - v = unhex(_hex) - return (v, Unit.COUNT) + v = unhex(_hex) + return (v, Unit.COUNT) # 0 to 100 % def percent(_hex): - v = unhex(_hex[0:2]) - v = v * 100.0 / 255.0 - return (v, Unit.PERCENT) + v = unhex(_hex[0:2]) + v = v * 100.0 / 255.0 + return (v, Unit.PERCENT) # -100 to 100 % def percent_centered(_hex): - v = unhex(_hex[0:2]) - v = (v - 128) * 100.0 / 128.0 - return (v, Unit.PERCENT) + v = unhex(_hex[0:2]) + v = (v - 128) * 100.0 / 128.0 + return (v, Unit.PERCENT) # -40 to 215 C def temp(_hex): - v = unhex(_hex) - v = v - 40 - return (v, Unit.C) + v = unhex(_hex) + v = v - 40 + return (v, Unit.C) # -40 to 6513.5 C def catalyst_temp(_hex): - v = unhex(_hex) - v = (v / 10.0) - 40 - return (v, Unit.C) + v = unhex(_hex) + v = (v / 10.0) - 40 + return (v, Unit.C) # -128 to 128 mA def current_centered(_hex): - v = unhex(_hex[4:8]) - v = (v / 256.0) - 128 - return (v, Unit.MA) + v = unhex(_hex[4:8]) + v = (v / 256.0) - 128 + return (v, Unit.MA) # 0 to 1.275 volts def sensor_voltage(_hex): - v = unhex(_hex[0:2]) - v = v / 200.0 - return (v, Unit.VOLT) + v = unhex(_hex[0:2]) + v = v / 200.0 + return (v, Unit.VOLT) # 0 to 8 volts def sensor_voltage_big(_hex): - v = unhex(_hex[4:8]) - v = (v * 8.0) / 65535 - return (v, Unit.VOLT) + v = unhex(_hex[4:8]) + v = (v * 8.0) / 65535 + return (v, Unit.VOLT) # 0 to 765 kPa def fuel_pressure(_hex): - v = unhex(_hex) - v = v * 3 - return (v, Unit.KPA) + v = unhex(_hex) + v = v * 3 + return (v, Unit.KPA) # 0 to 255 kPa def pressure(_hex): - v = unhex(_hex) - return (v, Unit.KPA) + v = unhex(_hex) + return (v, Unit.KPA) # 0 to 5177 kPa def fuel_pres_vac(_hex): - v = unhex(_hex) - v = v * 0.079 - return (v, Unit.KPA) + v = unhex(_hex) + v = v * 0.079 + return (v, Unit.KPA) # 0 to 655,350 kPa def fuel_pres_direct(_hex): - v = unhex(_hex) - v = v * 10 - return (v, Unit.KPA) + v = unhex(_hex) + v = v * 10 + return (v, Unit.KPA) # -8192 to 8192 Pa def evap_pressure(_hex): - # decode the twos complement - a = twos_comp(unhex(_hex[0:2]), 8) - b = twos_comp(unhex(_hex[2:4]), 8) - v = ((a * 256.0) + b) / 4.0 - return (v, Unit.PA) + # decode the twos complement + a = twos_comp(unhex(_hex[0:2]), 8) + b = twos_comp(unhex(_hex[2:4]), 8) + v = ((a * 256.0) + b) / 4.0 + return (v, Unit.PA) # 0 to 327.675 kPa def abs_evap_pressure(_hex): - v = unhex(_hex) - v = v / 200.0 - return (v, Unit.KPA) + v = unhex(_hex) + v = v / 200.0 + return (v, Unit.KPA) # -32767 to 32768 Pa def evap_pressure_alt(_hex): - v = unhex(_hex) - v = v - 32767 - return (v, Unit.PA) + v = unhex(_hex) + v = v - 32767 + return (v, Unit.PA) # 0 to 16,383.75 RPM def rpm(_hex): - v = unhex(_hex) - v = v / 4.0 - return (v, Unit.RPM) + v = unhex(_hex) + v = v / 4.0 + return (v, Unit.RPM) # 0 to 255 KPH def speed(_hex): - v = unhex(_hex) - return (v, Unit.KPH) + v = unhex(_hex) + return (v, Unit.KPH) # -64 to 63.5 degrees def timing_advance(_hex): - v = unhex(_hex) - v = (v - 128) / 2.0 - return (v, Unit.DEGREES) + v = unhex(_hex) + v = (v - 128) / 2.0 + return (v, Unit.DEGREES) # -210 to 301 degrees def inject_timing(_hex): - v = unhex(_hex) - v = (v - 26880) / 128.0 - return (v, Unit.DEGREES) + v = unhex(_hex) + v = (v - 26880) / 128.0 + return (v, Unit.DEGREES) # 0 to 655.35 grams/sec def maf(_hex): - v = unhex(_hex) - v = v / 100.0 - return (v, Unit.GPS) + v = unhex(_hex) + v = v / 100.0 + return (v, Unit.GPS) # 0 to 2550 grams/sec def max_maf(_hex): - v = unhex(_hex[0:2]) - v = v * 10 - return (v, Unit.GPS) + v = unhex(_hex[0:2]) + v = v * 10 + return (v, Unit.GPS) # 0 to 65535 seconds def seconds(_hex): - v = unhex(_hex) - return (v, Unit.SEC) + v = unhex(_hex) + return (v, Unit.SEC) # 0 to 65535 minutes def minutes(_hex): - v = unhex(_hex) - return (v, Unit.MIN) + v = unhex(_hex) + return (v, Unit.MIN) # 0 to 65535 km def distance(_hex): - v = unhex(_hex) - return (v, Unit.KM) + v = unhex(_hex) + return (v, Unit.KM) # 0 to 3212 Liters/hour def fuel_rate(_hex): - v = unhex(_hex) - v = v * 0.05 - return (v, Unit.LPH) + v = unhex(_hex) + v = v * 0.05 + return (v, Unit.LPH) ''' @@ -216,150 +216,150 @@ def fuel_rate(_hex): def status(_hex): - bits = bitstring(_hex, 32) + bits = bitstring(_hex, 32) - output = Status() - output.MIL = bitToBool(bits[0]) - output.DTC_count = unbin(bits[1:8]) - output.ignition_type = IGNITION_TYPE[unbin(bits[12])] + output = Status() + output.MIL = bitToBool(bits[0]) + output.DTC_count = unbin(bits[1:8]) + output.ignition_type = IGNITION_TYPE[unbin(bits[12])] - output.tests.append(Test("Misfire", \ - bitToBool(bits[15]), \ - bitToBool(bits[11]))) + output.tests.append(Test("Misfire", \ + bitToBool(bits[15]), \ + bitToBool(bits[11]))) - output.tests.append(Test("Fuel System", \ - bitToBool(bits[14]), \ - bitToBool(bits[10]))) + output.tests.append(Test("Fuel System", \ + bitToBool(bits[14]), \ + bitToBool(bits[10]))) - output.tests.append(Test("Components", \ - bitToBool(bits[13]), \ - bitToBool(bits[9]))) + output.tests.append(Test("Components", \ + bitToBool(bits[13]), \ + bitToBool(bits[9]))) - # different tests for different ignition types - if(output.ignition_type == IGNITION_TYPE[0]): # spark - for i in range(8): - if SPARK_TESTS[i] is not None: + # different tests for different ignition types + if(output.ignition_type == IGNITION_TYPE[0]): # spark + for i in range(8): + if SPARK_TESTS[i] is not None: - t = Test(SPARK_TESTS[i], \ - bitToBool(bits[(2 * 8) + i]), \ - bitToBool(bits[(3 * 8) + i])) + t = Test(SPARK_TESTS[i], \ + bitToBool(bits[(2 * 8) + i]), \ + bitToBool(bits[(3 * 8) + i])) - output.tests.append(t) + output.tests.append(t) - elif(output.ignition_type == IGNITION_TYPE[1]): # compression - for i in range(8): - if COMPRESSION_TESTS[i] is not None: + elif(output.ignition_type == IGNITION_TYPE[1]): # compression + for i in range(8): + if COMPRESSION_TESTS[i] is not None: - t = Test(COMPRESSION_TESTS[i], \ - bitToBool(bits[(2 * 8) + i]), \ - bitToBool(bits[(3 * 8) + i])) - - output.tests.append(t) + t = Test(COMPRESSION_TESTS[i], \ + bitToBool(bits[(2 * 8) + i]), \ + bitToBool(bits[(3 * 8) + i])) + + output.tests.append(t) - return (output, Unit.NONE) + return (output, Unit.NONE) def fuel_status(_hex): - v = unhex(_hex[0:2]) # todo, support second fuel system + v = unhex(_hex[0:2]) # todo, support second fuel system - if v <= 0: - debug("Invalid fuel status response (v <= 0)", True) - return (None, Unit.NONE) + if v <= 0: + debug("Invalid fuel status response (v <= 0)", True) + return (None, Unit.NONE) - i = math.log(v, 2) # only a single bit should be on + i = math.log(v, 2) # only a single bit should be on - if i % 1 != 0: - debug("Invalid fuel status response (multiple bits set)", True) - return (None, Unit.NONE) + if i % 1 != 0: + debug("Invalid fuel status response (multiple bits set)", True) + return (None, Unit.NONE) - i = int(i) + i = int(i) - if i >= len(FUEL_STATUS): - debug("Invalid fuel status response (no table entry)", True) - return (None, Unit.NONE) + if i >= len(FUEL_STATUS): + debug("Invalid fuel status response (no table entry)", True) + return (None, Unit.NONE) - return (FUEL_STATUS[i], Unit.NONE) + return (FUEL_STATUS[i], Unit.NONE) def air_status(_hex): - v = unhex(_hex) + v = unhex(_hex) - if v <= 0: - debug("Invalid air status response (v <= 0)", True) - return (None, Unit.NONE) + if v <= 0: + debug("Invalid air status response (v <= 0)", True) + return (None, Unit.NONE) - i = math.log(v, 2) # only a single bit should be on + i = math.log(v, 2) # only a single bit should be on - if i % 1 != 0: - debug("Invalid air status response (multiple bits set)", True) - return (None, Unit.NONE) + if i % 1 != 0: + debug("Invalid air status response (multiple bits set)", True) + return (None, Unit.NONE) - i = int(i) + i = int(i) - if i >= len(AIR_STATUS): - debug("Invalid air status response (no table entry)", True) - return (None, Unit.NONE) + if i >= len(AIR_STATUS): + debug("Invalid air status response (no table entry)", True) + return (None, Unit.NONE) - return (AIR_STATUS[i], Unit.NONE) + return (AIR_STATUS[i], Unit.NONE) def obd_compliance(_hex): - i = unhex(_hex) + i = unhex(_hex) - v = "Error: Unknown OBD compliance response" + v = "Error: Unknown OBD compliance response" - if i < len(OBD_COMPLIANCE): - v = OBD_COMPLIANCE[i] + if i < len(OBD_COMPLIANCE): + v = OBD_COMPLIANCE[i] - return (v, Unit.NONE) + return (v, Unit.NONE) def fuel_type(_hex): - i = unhex(_hex) + i = unhex(_hex) - v = "Error: Unknown fuel type response" + v = "Error: Unknown fuel type response" - if i < len(FUEL_TYPES): - v = FUEL_TYPES[i] + if i < len(FUEL_TYPES): + v = FUEL_TYPES[i] - return (v, Unit.NONE) + return (v, Unit.NONE) # converts 2 bytes of hex into a DTC code def single_dtc(_hex): - if len(_hex) != 4: - return None + if len(_hex) != 4: + return None - if _hex == "0000": - return None + if _hex == "0000": + return None - bits = bitstring(_hex[0], 4) + bits = bitstring(_hex[0], 4) - dtc = "" - dtc += ['P', 'C', 'B', 'U'][unbin(bits[0:2])] - dtc += str(unbin(bits[2:4])) - dtc += _hex[1:4] + dtc = "" + dtc += ['P', 'C', 'B', 'U'][unbin(bits[0:2])] + dtc += str(unbin(bits[2:4])) + dtc += _hex[1:4] - return dtc + return dtc # converts a frame of 2-byte DTCs into a list of DTCs # example input = "010480034123" # [ ][ ][ ] def dtc(_hex): - codes = [] - for n in range(0, len(_hex), 4): - dtc = single_dtc(_hex[n:n+4]) + codes = [] + for n in range(0, len(_hex), 4): + dtc = single_dtc(_hex[n:n+4]) - if dtc is not None: + if dtc is not None: - # pull a description if we have one - desc = "Unknown error code" - if dtc in DTC: - desc = DTC[dtc] + # pull a description if we have one + desc = "Unknown error code" + if dtc in DTC: + desc = DTC[dtc] - codes.append( (dtc, desc) ) + codes.append( (dtc, desc) ) - return (codes, Unit.NONE) + return (codes, Unit.NONE) diff --git a/obd/elm327.py b/obd/elm327.py index bc979ba3..cf071730 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -39,329 +39,329 @@ class ELM327: - """ - Provides interface for the vehicles primary ECU. - After instantiation with a portname (/dev/ttyUSB0, etc...), - the following functions become available: - - send_and_parse() - get_port_name() - is_connected() - close() - """ - - _SUPPORTED_PROTOCOLS = { - #"0" : None, # automatic mode - "1" : SAE_J1850_PWM, - "2" : SAE_J1850_VPW, - "3" : ISO_9141_2, - "4" : ISO_14230_4_5baud, - "5" : ISO_14230_4_fast, - "6" : ISO_15765_4_11bit_500k, - "7" : ISO_15765_4_29bit_500k, - "8" : ISO_15765_4_11bit_250k, - "9" : ISO_15765_4_29bit_250k, - "A" : SAE_J1939, - #"B" : None, # user defined 1 - #"C" : None, # user defined 2 - } - - def __init__(self, portname, baudrate=38400): - """Initializes port by resetting device and gettings supported PIDs. """ - - self.__connected = False - self.__port = None - self.__protocol = None - self.__primary_ecu = None # message.tx_id + """ + Provides interface for the vehicles primary ECU. + After instantiation with a portname (/dev/ttyUSB0, etc...), + the following functions become available: + + send_and_parse() + get_port_name() + is_connected() + close() + """ + + _SUPPORTED_PROTOCOLS = { + #"0" : None, # automatic mode + "1" : SAE_J1850_PWM, + "2" : SAE_J1850_VPW, + "3" : ISO_9141_2, + "4" : ISO_14230_4_5baud, + "5" : ISO_14230_4_fast, + "6" : ISO_15765_4_11bit_500k, + "7" : ISO_15765_4_29bit_500k, + "8" : ISO_15765_4_11bit_250k, + "9" : ISO_15765_4_29bit_250k, + "A" : SAE_J1939, + #"B" : None, # user defined 1 + #"C" : None, # user defined 2 + } + + def __init__(self, portname, baudrate=38400): + """Initializes port by resetting device and gettings supported PIDs. """ + + self.__connected = False + self.__port = None + self.__protocol = None + self.__primary_ecu = None # message.tx_id - # ------------- open port ------------- - - debug("Opening serial port '%s'" % portname) - - try: - self.__port = serial.Serial(portname, \ - baudrate = baudrate, \ - parity = serial.PARITY_NONE, \ - stopbits = 1, \ - bytesize = 8, \ - timeout = 1) # seconds - - except serial.SerialException as e: - self.__error(e) - return - except OSError as e: - self.__error(e) - return - - debug("Serial port successfully opened on " + self.get_port_name()) - - - # ---------------------------- ATZ (reset) ---------------------------- - try: - self.__send("ATZ", delay=1) # wait 1 second for ELM to initialize - # return data can be junk, so don't bother checking - except serial.SerialException as e: - self.__error(e) - return - - - # -------------------------- ATE0 (echo OFF) -------------------------- - r = self.__send("ATE0") - if not self.__isok(r, expectEcho=True): - self.__error("ATE0 did not return 'OK'") - return - - - # ------------------------- ATH1 (headers ON) ------------------------- - r = self.__send("ATH1") - if not self.__isok(r): - self.__error("ATH1 did not return 'OK', or echoing is still ON") - return - - - # ------------------------ ATL0 (linefeeds OFF) ----------------------- - r = self.__send("ATL0") - if not self.__isok(r): - self.__error("ATL0 did not return 'OK'") - return - - - # ---------------------- ATSPA8 (protocol AUTO) ----------------------- - r = self.__send("ATSPA8") - if not self.__isok(r): - self.__error("ATSPA8 did not return 'OK'") - return + # ------------- open port ------------- + + debug("Opening serial port '%s'" % portname) + + try: + self.__port = serial.Serial(portname, \ + baudrate = baudrate, \ + parity = serial.PARITY_NONE, \ + stopbits = 1, \ + bytesize = 8, \ + timeout = 1) # seconds + + except serial.SerialException as e: + self.__error(e) + return + except OSError as e: + self.__error(e) + return + + debug("Serial port successfully opened on " + self.get_port_name()) + + + # ---------------------------- ATZ (reset) ---------------------------- + try: + self.__send("ATZ", delay=1) # wait 1 second for ELM to initialize + # return data can be junk, so don't bother checking + except serial.SerialException as e: + self.__error(e) + return + + + # -------------------------- ATE0 (echo OFF) -------------------------- + r = self.__send("ATE0") + if not self.__isok(r, expectEcho=True): + self.__error("ATE0 did not return 'OK'") + return + + + # ------------------------- ATH1 (headers ON) ------------------------- + r = self.__send("ATH1") + if not self.__isok(r): + self.__error("ATH1 did not return 'OK', or echoing is still ON") + return + + + # ------------------------ ATL0 (linefeeds OFF) ----------------------- + r = self.__send("ATL0") + if not self.__isok(r): + self.__error("ATL0 did not return 'OK'") + return + + + # ---------------------- ATSPA8 (protocol AUTO) ----------------------- + r = self.__send("ATSPA8") + if not self.__isok(r): + self.__error("ATSPA8 did not return 'OK'") + return - # -------------- 0100 (first command, SEARCH protocols) -------------- - # TODO: rewrite this using a "wait for prompt character" - # rather than a fixed wait period - r0100 = self.__send("0100", delay=3) # give it a second (or three) to search + # -------------- 0100 (first command, SEARCH protocols) -------------- + # TODO: rewrite this using a "wait for prompt character" + # rather than a fixed wait period + r0100 = self.__send("0100", delay=3) # give it a second (or three) to search - # ------------------- ATDPN (list protocol number) ------------------- - r = self.__send("ATDPN") + # ------------------- ATDPN (list protocol number) ------------------- + r = self.__send("ATDPN") - if not r: - self.__error("Describe protocol command didn't return ") - return + if not r: + self.__error("Describe protocol command didn't return ") + return - p = r[0] + p = r[0] - # suppress any "automatic" prefix - p = p[1:] if (len(p) > 1 and p.startswith("A")) else p[:-1] + # suppress any "automatic" prefix + p = p[1:] if (len(p) > 1 and p.startswith("A")) else p[:-1] - if p not in self._SUPPORTED_PROTOCOLS: - self.__error("ELM responded with unknown protocol") - return + if p not in self._SUPPORTED_PROTOCOLS: + self.__error("ELM responded with unknown protocol") + return - # instantiate the correct protocol handler - self.__protocol = self._SUPPORTED_PROTOCOLS[p]() + # instantiate the correct protocol handler + self.__protocol = self._SUPPORTED_PROTOCOLS[p]() - # Now that a protocol has been selected, we can figure out - # which ECU is the primary. + # Now that a protocol has been selected, we can figure out + # which ECU is the primary. - m = self.__protocol(r0100) - self.__primary_ecu = self.__find_primary_ecu(m) - if self.__primary_ecu is None: - self.__error("Failed to choose primary ECU") - return + m = self.__protocol(r0100) + self.__primary_ecu = self.__find_primary_ecu(m) + if self.__primary_ecu is None: + self.__error("Failed to choose primary ECU") + return - # ------------------------------- done ------------------------------- - debug("Connection successful") - self.__connected = True + # ------------------------------- done ------------------------------- + debug("Connection successful") + self.__connected = True - def __isok(self, lines, expectEcho=False): - if not lines: - return False - if expectEcho: - return len(lines) == 2 and lines[1] == 'OK' - else: - return len(lines) == 1 and lines[0] == 'OK' + def __isok(self, lines, expectEcho=False): + if not lines: + return False + if expectEcho: + return len(lines) == 2 and lines[1] == 'OK' + else: + return len(lines) == 1 and lines[0] == 'OK' - def __find_primary_ecu(self, messages): - """ - Given a list of messages from different ECUS, - (in response to the 0100 PID listing command) - choose the ID of the primary ECU - """ + def __find_primary_ecu(self, messages): + """ + Given a list of messages from different ECUS, + (in response to the 0100 PID listing command) + choose the ID of the primary ECU + """ - if len(messages) == 0: - return None - elif len(messages) == 1: - return messages[0].tx_id - else: - # first, try filtering for the standard ECU IDs - test = lambda m: m.tx_id == self.__protocol.PRIMARY_ECU + if len(messages) == 0: + return None + elif len(messages) == 1: + return messages[0].tx_id + else: + # first, try filtering for the standard ECU IDs + test = lambda m: m.tx_id == self.__protocol.PRIMARY_ECU - if bool([m for m in messages if test(m)]): - return self.__protocol.PRIMARY_ECU - else: - # last resort solution, choose ECU - # with the most PIDs supported - best = 0 - tx_id = None + if bool([m for m in messages if test(m)]): + return self.__protocol.PRIMARY_ECU + else: + # last resort solution, choose ECU + # with the most PIDs supported + best = 0 + tx_id = None - for message in messages: - bits = sum([numBitsSet(b) for b in message.data_bytes]) + for message in messages: + bits = sum([numBitsSet(b) for b in message.data_bytes]) - if bits > best: - best = bits - tx_id = message.tx_id + if bits > best: + best = bits + tx_id = message.tx_id - return tx_id + return tx_id - def __error(self, msg=None): - """ handles fatal failures, print debug info and closes serial """ - - debug("Connection Error:", True) + def __error(self, msg=None): + """ handles fatal failures, print debug info and closes serial """ + + debug("Connection Error:", True) - if msg is not None: - debug(' ' + str(msg), True) + if msg is not None: + debug(' ' + str(msg), True) - if self.__port is not None: - self.__port.close() + if self.__port is not None: + self.__port.close() - self.__connected = False + self.__connected = False - def get_port_name(self): - return self.__port.portstr if (self.__port is not None) else "No Port" + def get_port_name(self): + return self.__port.portstr if (self.__port is not None) else "No Port" - def is_connected(self): - return self.__connected and (self.__port is not None) + def is_connected(self): + return self.__connected and (self.__port is not None) - def close(self): - """ - Resets the device, and clears all attributes to unconnected state - """ + def close(self): + """ + Resets the device, and clears all attributes to unconnected state + """ - if self.is_connected(): - self.__write("ATZ") - self.__port.close() + if self.is_connected(): + self.__write("ATZ") + self.__port.close() - self.__connected = False - self.__port = None - self.__protocol = None - self.__primary_ecu = None + self.__connected = False + self.__port = None + self.__protocol = None + self.__primary_ecu = None - def send_and_parse(self, cmd, delay=None): - """ - send() function used to service all OBDCommands + def send_and_parse(self, cmd, delay=None): + """ + send() function used to service all OBDCommands - Sends the given command string (rejects "AT" command), - parses the response string with the appropriate protocol object. + Sends the given command string (rejects "AT" command), + parses the response string with the appropriate protocol object. - Returns the Message object from the primary ECU, or None, - if no appropriate response was recieved. - """ + Returns the Message object from the primary ECU, or None, + if no appropriate response was recieved. + """ - if not self.is_connected(): - debug("cannot send_and_parse() when unconnected", True) - return None + if not self.is_connected(): + debug("cannot send_and_parse() when unconnected", True) + return None - if "AT" in cmd.upper(): - debug("Rejected sending AT command", True) - return None + if "AT" in cmd.upper(): + debug("Rejected sending AT command", True) + return None - lines = self.__send(cmd, delay) + lines = self.__send(cmd, delay) - # parses string into list of messages - messages = self.__protocol(lines) + # parses string into list of messages + messages = self.__protocol(lines) - # select the first message with the ECU ID we're looking for - # TODO: use ELM header settings to query ECU by address directly - for message in messages: - if message.tx_id == self.__primary_ecu: - return message + # select the first message with the ECU ID we're looking for + # TODO: use ELM header settings to query ECU by address directly + for message in messages: + if message.tx_id == self.__primary_ecu: + return message - return None # no suitable response was returned + return None # no suitable response was returned - def __send(self, cmd, delay=None): - """ - unprotected send() function + def __send(self, cmd, delay=None): + """ + unprotected send() function - will __write() the given string, no questions asked. - returns result of __read() after an optional delay. - """ + will __write() the given string, no questions asked. + returns result of __read() after an optional delay. + """ - self.__write(cmd) + self.__write(cmd) - if delay is not None: - debug("wait: %d seconds" % delay) - time.sleep(delay) + if delay is not None: + debug("wait: %d seconds" % delay) + time.sleep(delay) - return self.__read() + return self.__read() - def __write(self, cmd): - """ - "low-level" function to write a string to the port - """ + def __write(self, cmd): + """ + "low-level" function to write a string to the port + """ - if self.__port: - cmd += "\r\n" # terminate - self.__port.flushOutput() - self.__port.flushInput() - self.__port.write(cmd.encode()) # turn the string into bytes - debug("write: " + repr(cmd)) - else: - debug("cannot perform __write() when unconnected", True) + if self.__port: + cmd += "\r\n" # terminate + self.__port.flushOutput() + self.__port.flushInput() + self.__port.write(cmd.encode()) # turn the string into bytes + debug("write: " + repr(cmd)) + else: + debug("cannot perform __write() when unconnected", True) - def __read(self): - """ - "low-level" read function + def __read(self): + """ + "low-level" read function - accumulates characters until the prompt character is seen - returns a list of [/r/n] delimited strings - """ + accumulates characters until the prompt character is seen + returns a list of [/r/n] delimited strings + """ - attempts = 2 - buffer = b'' + attempts = 2 + buffer = b'' - if self.__port: - while True: - c = self.__port.read(1) + if self.__port: + while True: + c = self.__port.read(1) - # if nothing was recieved - if not c: + # if nothing was recieved + if not c: - if attempts <= 0: - break + if attempts <= 0: + break - debug("__read() found nothing") - attempts -= 1 - continue + debug("__read() found nothing") + attempts -= 1 + continue - # end on chevron (ELM prompt character) - if c == b'>': - break + # end on chevron (ELM prompt character) + if c == b'>': + break - # skip null characters (ELM spec page 9) - if c == b'\x00': - continue + # skip null characters (ELM spec page 9) + if c == b'\x00': + continue - buffer += c # whatever is left must be part of the response - else: - debug("cannot perform __read() when unconnected", True) - return "" + buffer += c # whatever is left must be part of the response + else: + debug("cannot perform __read() when unconnected", True) + return "" - debug("read: " + repr(buffer)) + debug("read: " + repr(buffer)) - # convert bytes into a standard string - raw = buffer.decode() + # convert bytes into a standard string + raw = buffer.decode() - # splits into lines - # removes empty lines - # removes trailing spaces - lines = [ s.strip() for s in re.split("[\r\n]", raw) if bool(s) ] + # splits into lines + # removes empty lines + # removes trailing spaces + lines = [ s.strip() for s in re.split("[\r\n]", raw) if bool(s) ] - return lines + return lines diff --git a/obd/obd.py b/obd/obd.py index 8f5ac7b5..11f5c226 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -38,141 +38,141 @@ class OBD(object): - """ class representing an OBD-II connection with it's assorted sensors """ + """ class representing an OBD-II connection with it's assorted sensors """ - def __init__(self, portstr=None, baudrate=38400): - self.port = None - self.supported_commands = [] + def __init__(self, portstr=None, baudrate=38400): + self.port = None + self.supported_commands = [] - debug("========================== Starting python-OBD ==========================") - self.connect(portstr, baudrate) # initialize by connecting and loading sensors - debug("=========================================================================") + debug("========================== Starting python-OBD ==========================") + self.connect(portstr, baudrate) # initialize by connecting and loading sensors + debug("=========================================================================") - def connect(self, portstr=None, baudrate=38400): - """ attempts to instantiate an ELM327 object. Loads commands on success""" + def connect(self, portstr=None, baudrate=38400): + """ attempts to instantiate an ELM327 object. Loads commands on success""" - if portstr is None: - debug("Using scanSerial to select port") - portnames = scanSerial() - debug("Available ports: " + str(portnames)) + if portstr is None: + debug("Using scanSerial to select port") + portnames = scanSerial() + debug("Available ports: " + str(portnames)) - for port in portnames: - debug("Attempting to use port: " + str(port)) - self.port = ELM327(port, baudrate=baudrate) + for port in portnames: + debug("Attempting to use port: " + str(port)) + self.port = ELM327(port, baudrate=baudrate) - if self.port.is_connected(): - # success! stop searching for serial - break - else: - debug("Explicit port defined") - self.port = ELM327(portstr, baudrate=baudrate) + if self.port.is_connected(): + # success! stop searching for serial + break + else: + debug("Explicit port defined") + self.port = ELM327(portstr, baudrate=baudrate) - # if a connection was made, query for commands - if self.is_connected(): - self.load_commands() - else: - debug("Failed to connect") + # if a connection was made, query for commands + if self.is_connected(): + self.load_commands() + else: + debug("Failed to connect") - def close(self): - if self.is_connected(): - debug("Closing connection") - self.port.close() - self.port = None + def close(self): + if self.is_connected(): + debug("Closing connection") + self.port.close() + self.port = None - def is_connected(self): - return (self.port is not None) and self.port.is_connected() + def is_connected(self): + return (self.port is not None) and self.port.is_connected() - def get_port_name(self): - if self.is_connected(): - return self.port.get_port_name() - else: - return "Not connected to any port" + def get_port_name(self): + if self.is_connected(): + return self.port.get_port_name() + else: + return "Not connected to any port" - def load_commands(self): - """ - queries for available PIDs, - sets their support status, - and compiles a list of command objects - """ + def load_commands(self): + """ + queries for available PIDs, + sets their support status, + and compiles a list of command objects + """ - debug("querying for supported PIDs (commands)...") + debug("querying for supported PIDs (commands)...") - self.supported_commands = [] + self.supported_commands = [] - pid_getters = commands.pid_getters() + pid_getters = commands.pid_getters() - for get in pid_getters: - # PID listing commands should sequentialy become supported - # Mode 1 PID 0 is assumed to always be supported - if not self.supports(get): - continue + for get in pid_getters: + # PID listing commands should sequentialy become supported + # Mode 1 PID 0 is assumed to always be supported + if not self.supports(get): + continue - response = self.send(get) # ask nicely + response = self.send(get) # ask nicely - if response.is_null(): - continue - - supported = response.value # string of binary 01010101010101 + if response.is_null(): + continue + + supported = response.value # string of binary 01010101010101 - # loop through PIDs binary - for i in range(len(supported)): - if supported[i] == "1": + # loop through PIDs binary + for i in range(len(supported)): + if supported[i] == "1": - mode = get.get_mode_int() - pid = get.get_pid_int() + i + 1 + mode = get.get_mode_int() + pid = get.get_pid_int() + i + 1 - if commands.has_pid(mode, pid): - c = commands[mode][pid] - c.supported = True + if commands.has_pid(mode, pid): + c = commands[mode][pid] + c.supported = True - # don't add PID getters to the command list - if c not in pid_getters: - self.supported_commands.append(c) + # don't add PID getters to the command list + if c not in pid_getters: + self.supported_commands.append(c) - debug("finished querying with %d commands supported" % len(self.supported_commands)) + debug("finished querying with %d commands supported" % len(self.supported_commands)) - def print_commands(self): - for c in self.supported_commands: - print(str(c)) + def print_commands(self): + for c in self.supported_commands: + print(str(c)) - def supports(self, c): - return commands.has_command(c) and c.supported + def supports(self, c): + return commands.has_command(c) and c.supported - def send(self, c): - """ send the given command, retrieve and parse response """ + def send(self, c): + """ send the given command, retrieve and parse response """ - if not self.is_connected(): - debug("Query failed, no connection available", True) - return Response() # return empty response + if not self.is_connected(): + debug("Query failed, no connection available", True) + return Response() # return empty response - debug("Sending command: %s" % str(c)) + debug("Sending command: %s" % str(c)) - # send command and retrieve message - m = self.port.send_and_parse(c.get_command()) + # send command and retrieve message + m = self.port.send_and_parse(c.get_command()) - if m is None: - return Response() # return empty response - else: - return c(m) # compute a response object - + if m is None: + return Response() # return empty response + else: + return c(m) # compute a response object + - def query(self, c, force=False): - """ - facade 'send' command function - protects against sending unsupported commands. - """ + def query(self, c, force=False): + """ + facade 'send' command function + protects against sending unsupported commands. + """ - # check that the command is supported - if self.supports(c) or force: - return self.send(c) - else: - debug("'%s' is not supported" % str(c), True) - return Response() # return empty response + # check that the command is supported + if self.supports(c) or force: + return self.send(c) + else: + debug("'%s' is not supported" % str(c), True) + return Response() # return empty response diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 95fd4527..1de6378f 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -35,103 +35,103 @@ class LegacyProtocol(Protocol): - PRIMARY_ECU = 0x10 + PRIMARY_ECU = 0x10 - def __init__(self, baud): - Protocol.__init__(self, baud) + def __init__(self, baud): + Protocol.__init__(self, baud) - def create_frame(self, raw): + def create_frame(self, raw): - frame = Frame(raw) - raw_bytes = ascii_to_bytes(raw) + frame = Frame(raw) + raw_bytes = ascii_to_bytes(raw) - if len(raw_bytes) < 6: - debug("Dropped frame for being too short") - return None + if len(raw_bytes) < 6: + debug("Dropped frame for being too short") + return None - if len(raw_bytes) > 11: - debug("Dropped frame for being too long") - return None + if len(raw_bytes) > 11: + debug("Dropped frame for being too long") + return None - # Ex. - # [Header] [ Frame ] - # 48 6B 10 41 00 BE 7F B8 13 ck - # ck = checksum byte + # Ex. + # [Header] [ Frame ] + # 48 6B 10 41 00 BE 7F B8 13 ck + # ck = checksum byte - # exclude header and trailing checksum (handled by ELM adapter) - frame.data_bytes = raw_bytes[3:-1] + # exclude header and trailing checksum (handled by ELM adapter) + frame.data_bytes = raw_bytes[3:-1] - # read header information - frame.priority = raw_bytes[0] - frame.rx_id = raw_bytes[1] - frame.tx_id = raw_bytes[2] + # read header information + frame.priority = raw_bytes[0] + frame.rx_id = raw_bytes[1] + frame.tx_id = raw_bytes[2] - return frame + return frame - def create_message(self, frames, tx_id): + def create_message(self, frames, tx_id): - message = Message(frames, tx_id) + message = Message(frames, tx_id) - # len(frames) will always be >= 1 (see the caller, protocol.py) - mode = frames[0].data_bytes[0] - - # test that all frames are responses to the same Mode (SID) - if len(frames) > 1: - if not all([mode == f.data_bytes[0] for f in frames[1:]]): - debug("Recieved frames from multiple commands") - return None + # len(frames) will always be >= 1 (see the caller, protocol.py) + mode = frames[0].data_bytes[0] + + # test that all frames are responses to the same Mode (SID) + if len(frames) > 1: + if not all([mode == f.data_bytes[0] for f in frames[1:]]): + debug("Recieved frames from multiple commands") + return None - # legacy protocols have different re-assembly - # procedures for different Modes + # legacy protocols have different re-assembly + # procedures for different Modes - if mode == 0x43: - # GET_DTC requests return frames with no PID or order bytes - # accumulate all of the data, minus the Mode bytes of each frame + if mode == 0x43: + # GET_DTC requests return frames with no PID or order bytes + # accumulate all of the data, minus the Mode bytes of each frame - # Ex. - # [ Frame ] - # 48 6B 10 43 03 00 03 02 03 03 ck - # 48 6B 10 43 03 04 00 00 00 00 ck - # [ Data ] + # Ex. + # [ Frame ] + # 48 6B 10 43 03 00 03 02 03 03 ck + # 48 6B 10 43 03 04 00 00 00 00 ck + # [ Data ] - for f in frames: - message.data_bytes += f.data_bytes[1:] + for f in frames: + message.data_bytes += f.data_bytes[1:] - else: - if len(frames) == 1: - # return data, excluding the mode/pid bytes + else: + if len(frames) == 1: + # return data, excluding the mode/pid bytes - # Ex. - # [ Frame ] - # 48 6B 10 41 00 BE 7F B8 13 ck - # [ Data ] + # Ex. + # [ Frame ] + # 48 6B 10 41 00 BE 7F B8 13 ck + # [ Data ] - message.data_bytes = frames[0].data_bytes[2:] + message.data_bytes = frames[0].data_bytes[2:] - else: # len(frames) > 1: - # generic multiline requests carry an order byte + else: # len(frames) > 1: + # generic multiline requests carry an order byte - # Ex. - # [ Frame ] - # 48 6B 10 49 02 01 00 00 00 31 ck - # 48 6B 10 49 02 02 44 34 47 50 ck - # 48 6B 10 49 02 03 30 30 52 35 ck - # etc... [] [ Data ] + # Ex. + # [ Frame ] + # 48 6B 10 49 02 01 00 00 00 31 ck + # 48 6B 10 49 02 02 44 34 47 50 ck + # 48 6B 10 49 02 03 30 30 52 35 ck + # etc... [] [ Data ] - # sort the frames by the order byte - frames = sorted(frames, key=lambda f: f.data_bytes[2]) + # sort the frames by the order byte + frames = sorted(frames, key=lambda f: f.data_bytes[2]) - # check contiguity - indices = [f.data_bytes[2] for f in frames] - if not contiguous(indices, 1, len(frames)): - debug("Recieved multiline response with missing frames") - return None + # check contiguity + indices = [f.data_bytes[2] for f in frames] + if not contiguous(indices, 1, len(frames)): + debug("Recieved multiline response with missing frames") + return None - # now that they're in order, accumulate the data from each frame - for f in frames: - message.data_bytes += f.data_bytes[3:] # loose the mode/pid/seq bytes + # now that they're in order, accumulate the data from each frame + for f in frames: + message.data_bytes += f.data_bytes[3:] # loose the mode/pid/seq bytes - return message + return message @@ -144,25 +144,25 @@ def create_message(self, frames, tx_id): class SAE_J1850_PWM(LegacyProtocol): - def __init__(self): - LegacyProtocol.__init__(self, baud=41600) + def __init__(self): + LegacyProtocol.__init__(self, baud=41600) class SAE_J1850_VPW(LegacyProtocol): - def __init__(self): - LegacyProtocol.__init__(self, baud=10400) + def __init__(self): + LegacyProtocol.__init__(self, baud=10400) class ISO_9141_2(LegacyProtocol): - def __init__(self): - LegacyProtocol.__init__(self, baud=10400) + def __init__(self): + LegacyProtocol.__init__(self, baud=10400) class ISO_14230_4_5baud(LegacyProtocol): - def __init__(self): - LegacyProtocol.__init__(self, baud=10400) + def __init__(self): + LegacyProtocol.__init__(self, baud=10400) class ISO_14230_4_fast(LegacyProtocol): - def __init__(self): - LegacyProtocol.__init__(self, baud=10400) + def __init__(self): + LegacyProtocol.__init__(self, baud=10400) diff --git a/obd/utils.py b/obd/utils.py index b18609f6..84195185 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -39,173 +39,173 @@ class Unit: - NONE = None - RATIO = "Ratio" - COUNT = "Count" - PERCENT = "%" - RPM = "RPM" - VOLT = "Volt" - F = "F" - C = "C" - SEC = "Second" - MIN = "Minute" - PA = "Pa" - KPA = "kPa" - PSI = "psi" - KPH = "kph" - MPH = "mph" - DEGREES = "Degrees" - GPS = "Grams per Second" - MA = "mA" - KM = "km" - LPH = "Liters per Hour" + NONE = None + RATIO = "Ratio" + COUNT = "Count" + PERCENT = "%" + RPM = "RPM" + VOLT = "Volt" + F = "F" + C = "C" + SEC = "Second" + MIN = "Minute" + PA = "Pa" + KPA = "kPa" + PSI = "psi" + KPH = "kph" + MPH = "mph" + DEGREES = "Degrees" + GPS = "Grams per Second" + MA = "mA" + KM = "km" + LPH = "Liters per Hour" class Response(): - def __init__(self, command=None, message=None): - self.command = command - self.message = message - self.value = None - self.unit = Unit.NONE - self.time = time.time() + def __init__(self, command=None, message=None): + self.command = command + self.message = message + self.value = None + self.unit = Unit.NONE + self.time = time.time() - def is_null(self): - return (self.message == None) or (self.value == None) + def is_null(self): + return (self.message == None) or (self.value == None) - def __str__(self): - return "%s %s" % (str(self.value), str(self.unit)) + def __str__(self): + return "%s %s" % (str(self.value), str(self.unit)) class Status(): - def __init__(self): - self.MIL = False - self.DTC_count = 0 - self.ignition_type = "" - self.tests = [] + def __init__(self): + self.MIL = False + self.DTC_count = 0 + self.ignition_type = "" + self.tests = [] class Test(): - def __init__(self, name, available, incomplete): - self.name = name - self.available = available - self.incomplete = incomplete + def __init__(self, name, available, incomplete): + self.name = name + self.available = available + self.incomplete = incomplete - def __str__(self): - a = "Available" if self.available else "Unavailable" - c = "Incomplete" if self.incomplete else "Complete" - return "Test %s: %s, %s" % (self.name, a, c) + def __str__(self): + a = "Available" if self.available else "Unavailable" + c = "Incomplete" if self.incomplete else "Complete" + return "Test %s: %s, %s" % (self.name, a, c) def ascii_to_bytes(a): - b = [] - for i in range(0, len(a), 2): - b.append(int(a[i:i+2], 16)) - return b + b = [] + for i in range(0, len(a), 2): + b.append(int(a[i:i+2], 16)) + return b def numBitsSet(n): - # TODO: there must be a better way to do this... - total = 0 - ref = 1 - for b in range(8): - total += int(bool(n & ref)) - ref = ref << 1 - return total + # TODO: there must be a better way to do this... + total = 0 + ref = 1 + for b in range(8): + total += int(bool(n & ref)) + ref = ref << 1 + return total def unhex(_hex): - _hex = "0" if _hex == "" else _hex - return int(_hex, 16) + _hex = "0" if _hex == "" else _hex + return int(_hex, 16) def unbin(_bin): - return int(_bin, 2) + return int(_bin, 2) def bitstring(_hex, bits=None): - b = bin(unhex(_hex))[2:] - if bits is not None: - b = ('0' * (bits - len(b))) + b - return b + b = bin(unhex(_hex))[2:] + if bits is not None: + b = ('0' * (bits - len(b))) + b + return b def bitToBool(_bit): - return (_bit == '1') + return (_bit == '1') def twos_comp(val, num_bits): - """compute the 2's compliment of int value val""" - if( (val&(1<<(num_bits-1))) != 0 ): - val = val - (1< 0: - debug("Receieved less data than expected, trying to parse anyways...") - _hex += ('0' * diff) # pad the right side with zeros - elif diff < 0: - debug("Receieved more data than expected, trying to parse anyways...") - _hex = _hex[:diff] # chop off the right side to fit + if diff > 0: + debug("Receieved less data than expected, trying to parse anyways...") + _hex += ('0' * diff) # pad the right side with zeros + elif diff < 0: + debug("Receieved more data than expected, trying to parse anyways...") + _hex = _hex[:diff] # chop off the right side to fit - return _hex + return _hex # checks that a list of integers are consequtive def contiguous(l, start, end): - if not l: - return False - if l[0] != start: - return False - if l[-1] != end: - return False + if not l: + return False + if l[0] != start: + return False + if l[-1] != end: + return False - # for consequtiveness, look at the integers in pairs - pairs = zip(l, l[1:]) - if not all([p[0]+1 == p[1] for p in pairs]): - return False + # for consequtiveness, look at the integers in pairs + pairs = zip(l, l[1:]) + if not all([p[0]+1 == p[1] for p in pairs]): + return False - return True + return True def try_port(portStr): - """returns boolean for port availability""" - try: - s = serial.Serial(portStr) - s.close() # explicit close 'cause of delayed GC in java - return True + """returns boolean for port availability""" + try: + s = serial.Serial(portStr) + s.close() # explicit close 'cause of delayed GC in java + return True - except serial.SerialException: - pass - except OSError as e: - if e.errno != errno.ENOENT: # permit "no such file or directory" errors - raise e + except serial.SerialException: + pass + except OSError as e: + if e.errno != errno.ENOENT: # permit "no such file or directory" errors + raise e - return False + return False def scanSerial(): - """scan for available ports. return a list of serial names""" - available = [] + """scan for available ports. return a list of serial names""" + available = [] - possible_ports = [] + possible_ports = [] - if sys.platform.startswith('linux') or sys.platform.startswith('cygwin'): - possible_ports += glob.glob("/dev/rfcomm[0-9]*") - possible_ports += glob.glob("/dev/ttyUSB[0-9]*") + if sys.platform.startswith('linux') or sys.platform.startswith('cygwin'): + possible_ports += glob.glob("/dev/rfcomm[0-9]*") + possible_ports += glob.glob("/dev/ttyUSB[0-9]*") - elif sys.platform.startswith('win'): - possible_ports += ["\\.\COM%d" % i for i in range(256)] + elif sys.platform.startswith('win'): + possible_ports += ["\\.\COM%d" % i for i in range(256)] - elif sys.platform.startswith('darwin'): - exclude = [ - '/dev/tty.Bluetooth-Incoming-Port', - '/dev/tty.Bluetooth-Modem' - ] - possible_ports += [port for port in glob.glob('/dev/tty.*') if port not in exclude] + elif sys.platform.startswith('darwin'): + exclude = [ + '/dev/tty.Bluetooth-Incoming-Port', + '/dev/tty.Bluetooth-Modem' + ] + possible_ports += [port for port in glob.glob('/dev/tty.*') if port not in exclude] - # possible_ports += glob.glob('/dev/pts/[0-9]*') # for obdsim + # possible_ports += glob.glob('/dev/pts/[0-9]*') # for obdsim - for port in possible_ports: - if try_port(port): - available.append(port) - - return available + for port in possible_ports: + if try_port(port): + available.append(port) + + return available From e77082ce1d8ad1979ce75440498948bea09616f3 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 22 Mar 2015 17:44:08 -0400 Subject: [PATCH 197/569] added some notes to the protocol readme --- obd/protocols/README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/obd/protocols/README.md b/obd/protocols/README.md index c69ef33d..15f02a18 100644 --- a/obd/protocols/README.md +++ b/obd/protocols/README.md @@ -1,5 +1,42 @@ +Notes +----- + +Each protocol object is callable, and accepts a list of raw input strings, and returns a list of parsed `Message` objects. The `data_bytes` field will contain a list of integers, corresponding to all relevant data returned by the command. + +*Note: `Message.data_bytes` does not refer to the full data field of a message, but rather a subset of this field. Things like Mode/PID/PCI bytes are removed. However, `Frame.data_bytes` DOES include the full data field (per-spec), for each frame.* + +For example, these are the resultant `Message.data_bytes` fields for some single frame messages: + +``` +A CAN Message: +7E8 06 41 00 BE 7F B8 13 + [ data ] + +A J1850 Message: +48 6B 10 41 00 BE 7F B8 13 FF + [ data ] +``` + +Subclassing `Protocol` +--------------------- + +All protocol objects must implement two functions: + +---------------------------------------- + +#### create_frame(self, raw) + +Recieves a single frame (in string form), and is responsible for parsing and returning a new `Frame` object. If the frame is invalid, or the parse fails, this function should return `None`, and the frame will be dropped. + +---------------------------------------- + +#### create_message(self, frames, tx_id) + +Recieves a list of `Frame`s, and is responsible for assembling them into a finished `Message` object. This is where multi-line responses are assembled, and the final `Message.data_bytes` field is filled. If the message is found to be invalid, this function should return `None`, and the entire message will be dropped. + Inheritance structure +--------------------- ``` Protocol From 7a909517dd55f05927aafd6d68b34c464d380941 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 22 Mar 2015 18:18:58 -0400 Subject: [PATCH 198/569] added sexy block diagram of data flow --- obd/README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 obd/README.md diff --git a/obd/README.md b/obd/README.md new file mode 100644 index 00000000..4090b74f --- /dev/null +++ b/obd/README.md @@ -0,0 +1,30 @@ + +Notes +----- + +Here's how it works: + +``` +┌───────────────────────┐ +│ obd.py (API) │ +└───┰───────────────────┘ + ┃ ▲ + ┃ ┃ +┌───╂───────────────╂───┐ ┌───────────────────────┐ +│ ┃ ┗━━━┿━━━━━━┥ │ +│ ┃ OBDCommand.py │ │ decoders.py │ +│ ┃ ┏━━━┿━━━━ ▶│ │ +└───╂───────────────╂───┘ └───────────────────────┘ + ┃ ┃ + ┃ ┃ +┌───╂───────────────╂───┐ ┌───────────────────────┐ +│ ┃ ┗━━━┿━━━━━━┥ │ +│ ┃ elm327.py │ │ protocol/ │ +│ ┃ ┏━━━┿━━━━ ▶│ │ +└───╂───────────────╂───┘ └───────────────────────┘ + ┃ ┃ + ▼ ┃ +┌───────────────────┸───┐ +│ pyserial │ +└───────────────────────┘ +``` From 339a3a44cb240de78381157144bd2aed9ae51c6a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 22 Mar 2015 22:15:25 -0400 Subject: [PATCH 199/569] increased port timeout, and removed silly 3 second wait during init --- obd/elm327.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index cf071730..4e4105be 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -84,7 +84,7 @@ def __init__(self, portname, baudrate=38400): parity = serial.PARITY_NONE, \ stopbits = 1, \ bytesize = 8, \ - timeout = 1) # seconds + timeout = 3) # seconds except serial.SerialException as e: self.__error(e) @@ -136,7 +136,7 @@ def __init__(self, portname, baudrate=38400): # -------------- 0100 (first command, SEARCH protocols) -------------- # TODO: rewrite this using a "wait for prompt character" # rather than a fixed wait period - r0100 = self.__send("0100", delay=3) # give it a second (or three) to search + r0100 = self.__send("0100") # ------------------- ATDPN (list protocol number) ------------------- @@ -335,6 +335,7 @@ def __read(self): if not c: if attempts <= 0: + debug("__read() never recieved prompt character") break debug("__read() found nothing") From 3eafc8b243df8c73d30861e9514180e5e9ff6082 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 22 Mar 2015 22:45:40 -0400 Subject: [PATCH 200/569] more minor formatting tweaks --- obd/README.md | 22 ++++++++++------------ obd/protocols/__init__.py | 16 ++++++++-------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/obd/README.md b/obd/README.md index 4090b74f..98aa3cc3 100644 --- a/obd/README.md +++ b/obd/README.md @@ -2,26 +2,24 @@ Notes ----- -Here's how it works: - ``` ┌───────────────────────┐ │ obd.py (API) │ └───┰───────────────────┘ ┃ ▲ ┃ ┃ -┌───╂───────────────╂───┐ ┌───────────────────────┐ -│ ┃ ┗━━━┿━━━━━━┥ │ -│ ┃ OBDCommand.py │ │ decoders.py │ -│ ┃ ┏━━━┿━━━━ ▶│ │ -└───╂───────────────╂───┘ └───────────────────────┘ +┌───╂───────────────╂───┐ ┌─────────────────┐ +│ ┃ ┗━━━┿━━━━━━┥ │ +│ ┃ OBDCommand.py │ │ decoders.py │ +│ ┃ ┏━━━┿━━━━ ▶│ │ +└───╂───────────────╂───┘ └─────────────────┘ ┃ ┃ ┃ ┃ -┌───╂───────────────╂───┐ ┌───────────────────────┐ -│ ┃ ┗━━━┿━━━━━━┥ │ -│ ┃ elm327.py │ │ protocol/ │ -│ ┃ ┏━━━┿━━━━ ▶│ │ -└───╂───────────────╂───┘ └───────────────────────┘ +┌───╂───────────────╂───┐ ┌─────────────────┐ +│ ┃ ┗━━━┿━━━━━━┥ │ +│ ┃ elm327.py │ │ protocol/ │ +│ ┃ ┏━━━┿━━━━ ▶│ │ +└───╂───────────────╂───┘ └─────────────────┘ ┃ ┃ ▼ ┃ ┌───────────────────┸───┐ diff --git a/obd/protocols/__init__.py b/obd/protocols/__init__.py index e35d0ee0..1165e841 100644 --- a/obd/protocols/__init__.py +++ b/obd/protocols/__init__.py @@ -30,13 +30,13 @@ ######################################################################## from .protocol_legacy import SAE_J1850_PWM, \ - SAE_J1850_VPW, \ - ISO_9141_2, \ - ISO_14230_4_5baud, \ - ISO_14230_4_fast + SAE_J1850_VPW, \ + ISO_9141_2, \ + ISO_14230_4_5baud, \ + ISO_14230_4_fast from .protocol_can import ISO_15765_4_11bit_500k, \ - ISO_15765_4_29bit_500k, \ - ISO_15765_4_11bit_250k, \ - ISO_15765_4_29bit_250k, \ - SAE_J1939 + ISO_15765_4_29bit_500k, \ + ISO_15765_4_11bit_250k, \ + ISO_15765_4_29bit_250k, \ + SAE_J1939 From 81fe93b0523237005bf86a97077f7c22d4a90c5e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 28 Mar 2015 15:06:04 -0400 Subject: [PATCH 201/569] accept python2 unicode objects as lookup keys --- obd/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index f4e39b91..291b92e8 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -203,7 +203,7 @@ def __init__(self): def __getitem__(self, key): if isinstance(key, int): return self.modes[key] - elif isinstance(key, str): + elif isinstance(key, str) or isinstance(key, unicode): return self.__dict__[key] else: debug("OBD commands can only be retrieved by PID value or dict name", True) @@ -250,7 +250,7 @@ def has_command(self, c): # checks for existance of command by name def has_name(self, s): - if isinstance(s, str): + if isinstance(s, str) or isinstance(s, unicode): return s.isupper() and (s in self.__dict__.keys()) else: debug("has_name() only accepts string names for commands", True) From 856b5de96ea17981ee94ed937f8cf70b9981d5ca Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 10 Apr 2015 23:07:59 -0400 Subject: [PATCH 202/569] set GET_DTC command for arbitrary number of response bytes --- obd/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/commands.py b/obd/commands.py index 291b92e8..8587da62 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -160,7 +160,7 @@ __mode3__ = [ # sensor name description mode cmd bytes decoder - OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 6, dtc , True), + OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 0, dtc , True), ] __mode4__ = [ From 82517559be0497ffa02845aef0a8b00a87a7d6bd Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 19 Apr 2015 15:01:15 -0400 Subject: [PATCH 203/569] added better docstrings, made some functions private --- obd/__init__.py | 7 +++++++ obd/async.py | 4 +++- obd/obd.py | 50 +++++++++++++++++++++++++++++++++---------------- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/obd/__init__.py b/obd/__init__.py index 26204a4c..bb853496 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -1,4 +1,11 @@ +""" + A serial module for accessing data from a vehicles OBD-II port + + For more documentation, visit: + https://github.com/brendanwhitfield/python-OBD/wiki +""" + ######################################################################## # # # python-OBD: A python OBD-II serial module derived from pyobd # diff --git a/obd/async.py b/obd/async.py index b5176ce7..5e76060e 100644 --- a/obd/async.py +++ b/obd/async.py @@ -143,7 +143,9 @@ def run(self): if len(self.commands) > 0: # loop over the requested commands, send, and collect the response for c in self.commands: - r = self.send(c) + + # force, since commands are checked for support in watch() + r = super(Async, self).query(c, force=True) # store the response self.commands[c] = r diff --git a/obd/obd.py b/obd/obd.py index 11f5c226..f7c554bf 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -30,6 +30,8 @@ ######################################################################## import time + +from obd import __version__ from .elm327 import ELM327 from .commands import commands from .utils import scanSerial, Response @@ -38,19 +40,24 @@ class OBD(object): - """ class representing an OBD-II connection with it's assorted sensors """ + """ + Class representing an OBD-II connection with it's assorted commands/sensors + """ def __init__(self, portstr=None, baudrate=38400): self.port = None self.supported_commands = [] - debug("========================== Starting python-OBD ==========================") - self.connect(portstr, baudrate) # initialize by connecting and loading sensors + debug("========================== python-OBD (v%s) ==========================" % __version__) + self.__connect(portstr, baudrate) # initialize by connecting and loading sensors debug("=========================================================================") - def connect(self, portstr=None, baudrate=38400): - """ attempts to instantiate an ELM327 object. Loads commands on success""" + def __connect(self, portstr=None, baudrate=38400): + """ + Attempts to instantiate an ELM327 connection object. + Upon success, __load_commands() is called + """ if portstr is None: debug("Using scanSerial to select port") @@ -70,34 +77,37 @@ def connect(self, portstr=None, baudrate=38400): # if a connection was made, query for commands if self.is_connected(): - self.load_commands() + self.__load_commands() else: debug("Failed to connect") def close(self): + """ Closes the connection """ if self.is_connected(): debug("Closing connection") self.port.close() self.port = None + self.supported_commands = [] def is_connected(self): + """ Returns a boolean for whether a successful serial connection was made """ return (self.port is not None) and self.port.is_connected() def get_port_name(self): + """ Returns the name of the currently connected port """ if self.is_connected(): return self.port.get_port_name() else: return "Not connected to any port" - def load_commands(self): + def __load_commands(self): """ - queries for available PIDs, - sets their support status, - and compiles a list of command objects + Queries for available PIDs, sets their support status, + and compiles a list of command objects. """ debug("querying for supported PIDs (commands)...") @@ -112,7 +122,7 @@ def load_commands(self): if not self.supports(get): continue - response = self.send(get) # ask nicely + response = self.__send(get) # ask nicely if response.is_null(): continue @@ -138,16 +148,24 @@ def load_commands(self): def print_commands(self): + """ + Utility function meant for working in interactive mode. + Prints all commands supported by the car. + """ for c in self.supported_commands: print(str(c)) def supports(self, c): + """ Returns a boolean for whether the car supports the given command """ return commands.has_command(c) and c.supported - def send(self, c): - """ send the given command, retrieve and parse response """ + def __send(self, c): + """ + Back-end implementation of query() + sends the given command, retrieves and parses the response + """ if not self.is_connected(): debug("Query failed, no connection available", True) @@ -162,17 +180,17 @@ def send(self, c): return Response() # return empty response else: return c(m) # compute a response object - + def query(self, c, force=False): """ - facade 'send' command function + primary API function. Sends commands to the car, and protects against sending unsupported commands. """ # check that the command is supported if self.supports(c) or force: - return self.send(c) + return self.__send(c) else: debug("'%s' is not supported" % str(c), True) return Response() # return empty response From b0991222a7ac29a03a08d5047d6616a463ae0955 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 19 Apr 2015 15:14:36 -0400 Subject: [PATCH 204/569] more docstrings for Async class, decreased idle wait time --- obd/async.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/obd/async.py b/obd/async.py index 5e76060e..8240e61b 100644 --- a/obd/async.py +++ b/obd/async.py @@ -36,7 +36,10 @@ from . import OBD class Async(OBD): - """ subclass representing an OBD-II connection """ + """ + Class representing an OBD-II connection with it's assorted commands/sensors + Specialized for asynchronous value reporting. + """ def __init__(self, portstr=None, baudrate=38400): super(Async, self).__init__(portstr, baudrate) @@ -47,6 +50,7 @@ def __init__(self, portstr=None, baudrate=38400): def start(self): + """ Starts the async update loop """ if self.is_connected(): debug("Starting async thread") self.running = True @@ -58,6 +62,7 @@ def start(self): def stop(self): + """ Stops the async update loop """ if self.thread is not None: debug("Stopping async thread...") self.running = False @@ -67,11 +72,17 @@ def stop(self): def close(self): + """ Closes the connection """ self.stop() super(Async, self).close() def watch(self, c, callback=None, force=False): + """ + Subscribes the given command for continuous updating. Once subscribed, + query() will return that command's latest value. Optional callbacks can + be given, which will be fired upon every new value. + """ # the dict shouldn't be changed while the daemon thread is iterating if self.running: @@ -95,6 +106,11 @@ def watch(self, c, callback=None, force=False): def unwatch(self, c, callback=None): + """ + Unsubscribes a specific command (and optionally, a specific callback) + from being updated. If no callback is specified, all callbacks for + that command are dropped. + """ # the dict shouldn't be changed while the daemon thread is iterating if self.running: @@ -117,6 +133,7 @@ def unwatch(self, c, callback=None): def unwatch_all(self): + """ Unsubscribes all commands and callbacks from being updated """ # the dict shouldn't be changed while the daemon thread is iterating if self.running: @@ -128,6 +145,11 @@ def unwatch_all(self): def query(self, c): + """ + Non-blocking query(). + Only commands that have been watch()ed will return valid responses + """ + if c in self.commands: return self.commands[c] else: @@ -155,4 +177,4 @@ def run(self): callback(r) else: - time.sleep(1) # idle + time.sleep(0.25) # idle From 38618a4dc42896b5193e6a7efcfe906005644f18 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 19 Apr 2015 15:22:48 -0400 Subject: [PATCH 205/569] more docstrings for commands.py --- obd/commands.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 8587da62..09cb0236 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -201,6 +201,14 @@ def __init__(self): def __getitem__(self, key): + """ + commands can be accessed by name, or by mode/pid + + obd.commands.RPM + obd.commands["RPM"] + obd.commands[1][12] # mode 1, PID 12 (RPM) + """ + if isinstance(key, int): return self.modes[key] elif isinstance(key, str) or isinstance(key, unicode): @@ -210,6 +218,7 @@ def __getitem__(self, key): def __len__(self): + """ returns the number of commands supported by python-OBD """ l = 0 for m in self.modes: l += len(m) @@ -217,11 +226,12 @@ def __len__(self): def __contains__(self, s): + """ calls has_name(s) """ return self.has_name(s) - # returns a list of PID GET commands def pid_getters(self): + """ returns a list of PID GET commands """ getters = [] for m in self.modes: for c in m: @@ -230,8 +240,8 @@ def pid_getters(self): return getters - # sets the boolean supported flag for the given command def set_supported(self, mode, pid, v): + """ sets the boolean supported flag for the given command """ if isinstance(v, bool): if self.has(mode, pid): self.modes[mode][pid].supported = v @@ -239,8 +249,8 @@ def set_supported(self, mode, pid, v): debug("set_supported() only accepts boolean values", True) - # checks for existance of command by OBDCommand object def has_command(self, c): + """ checks for existance of a command by OBDCommand object """ if isinstance(c, OBDCommand): return c in self.__dict__.values() else: @@ -248,8 +258,8 @@ def has_command(self, c): return False - # checks for existance of command by name def has_name(self, s): + """ checks for existance of a command by name """ if isinstance(s, str) or isinstance(s, unicode): return s.isupper() and (s in self.__dict__.keys()) else: @@ -257,8 +267,8 @@ def has_name(self, s): return False - # checks for existance of int mode and int pid def has_pid(self, mode, pid): + """ checks for existance of a command by int mode and int pid """ if isinstance(mode, int) and isinstance(pid, int): if (mode < 0) or (pid < 0): return False From 2f611a69664c213f6c45a89c4781e24d70ef88f3 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 19 Apr 2015 16:04:30 -0400 Subject: [PATCH 206/569] added __version__ file, Async.start() handles cases where no commands are subscribed --- obd/__init__.py | 3 +-- obd/__version__.py | 2 ++ obd/async.py | 13 ++++++++----- obd/obd.py | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 obd/__version__.py diff --git a/obd/__init__.py b/obd/__init__.py index bb853496..74153872 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -36,8 +36,7 @@ # # ######################################################################## -__version__ = '0.3.0' - +from .__version__ import __version__ from .obd import OBD from .OBDCommand import OBDCommand from .commands import commands diff --git a/obd/__version__.py b/obd/__version__.py new file mode 100644 index 00000000..aed0de48 --- /dev/null +++ b/obd/__version__.py @@ -0,0 +1,2 @@ + +__version__ = '0.3.0' diff --git a/obd/async.py b/obd/async.py index 8240e61b..5cc9a529 100644 --- a/obd/async.py +++ b/obd/async.py @@ -52,11 +52,14 @@ def __init__(self, portstr=None, baudrate=38400): def start(self): """ Starts the async update loop """ if self.is_connected(): - debug("Starting async thread") - self.running = True - self.thread = threading.Thread(target=self.run) - self.thread.daemon = True - self.thread.start() + if len(self.commands) > 0: + debug("Starting async thread") + self.running = True + self.thread = threading.Thread(target=self.run) + self.thread.daemon = True + self.thread.start() + else: + debug("Async thread not started because no commands were registered") else: debug("Async thread not started because no connection was made") diff --git a/obd/obd.py b/obd/obd.py index f7c554bf..96ba6943 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -31,7 +31,7 @@ import time -from obd import __version__ +from .__version__ import __version__ from .elm327 import ELM327 from .commands import commands from .utils import scanSerial, Response From 2e20002d65b37e04f6086e86224a08f140e8ccfa Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 23 Apr 2015 08:49:03 -0400 Subject: [PATCH 207/569] handle DTC count byte in CAN GET_DTC responses --- obd/protocols/protocol_can.py | 9 +++++++-- tests/test_protocol_can.py | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 05eafe46..890fa6d6 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -188,8 +188,13 @@ def create_message(self, frames, tx_id): # chop off the Mode/PID bytes based on the mode number mode = message.data_bytes[0] if mode == 0x43: - # GET_DTC requests (mode 03) do not have a PID byte - message.data_bytes = message.data_bytes[1:] + + # fetch the DTC count, and use it as a length code + num_dtc_bytes = message.data_bytes[1] * 2 + + # skip the PID byte and the DTC count, + message.data_bytes = message.data_bytes[2:][:num_dtc_bytes] + else: # handles cases when there is both a Mode and PID byte message.data_bytes = message.data_bytes[2:] diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index 7fc8065f..8799f1f6 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -133,11 +133,11 @@ def test_multi_line(): # MODE 03 COMMANDS (GET_DTC) RETURN NO PID BYTE test_case = [ - "7E8 10 20 43 00 01 02 03 04", - "7E8 21 05 06 07 08 09 0A 0B", + "7E8 10 20 43 04 00 01 02 03", + "7E8 21 04 05 06 07 08 09 0A", ] - correct_data = list(range(12)) + correct_data = list(range(8)) r = p(test_case) assert len(r) == 1 From df800ae2ba32032eb8a32212392af6524e64b590 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Apr 2015 18:54:26 -0400 Subject: [PATCH 208/569] don't print the unit field if it's undefined --- obd/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/obd/utils.py b/obd/utils.py index 84195185..c98fa6f7 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -73,7 +73,10 @@ def is_null(self): return (self.message == None) or (self.value == None) def __str__(self): - return "%s %s" % (str(self.value), str(self.unit)) + if self.unit != Unit.NONE: + return "%s %s" % (str(self.value), str(self.unit)) + else: + return str(self.value) class Status(): From df254dfb91311c025a6316433d9d874e6b449cad Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 4 May 2015 20:32:33 -0400 Subject: [PATCH 209/569] bump to version 0.4.0 --- obd/__version__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/__version__.py b/obd/__version__.py index aed0de48..652a8f47 100644 --- a/obd/__version__.py +++ b/obd/__version__.py @@ -1,2 +1,2 @@ -__version__ = '0.3.0' +__version__ = '0.4.0' diff --git a/setup.py b/setup.py index 4245a1bf..a43a4f5c 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="obd", - version="0.3.0", + version="0.4.0", description=("Serial module for handling live sensor data from a vehicle's OBD-II port"), classifiers=[ "Operating System :: POSIX :: Linux", From bcc48e82126c94934e31c721a13e5ac8ae4dbf24 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 25 May 2015 21:46:44 -0400 Subject: [PATCH 210/569] removed duplicate default params, fixed GPL header filename --- obd/elm327.py | 4 ++-- obd/obd.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 4e4105be..fa46b181 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -10,7 +10,7 @@ # # ######################################################################## # # -# port.py # +# elm327.py # # # # This file is part of python-OBD (a derivative of pyOBD) # # # @@ -66,7 +66,7 @@ class ELM327: #"C" : None, # user defined 2 } - def __init__(self, portname, baudrate=38400): + def __init__(self, portname, baudrate): """Initializes port by resetting device and gettings supported PIDs. """ self.__connected = False diff --git a/obd/obd.py b/obd/obd.py index 96ba6943..3ef4ff6d 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -53,7 +53,7 @@ def __init__(self, portstr=None, baudrate=38400): debug("=========================================================================") - def __connect(self, portstr=None, baudrate=38400): + def __connect(self, portstr, baudrate): """ Attempts to instantiate an ELM327 connection object. Upon success, __load_commands() is called From 259eb4a8a0cbf07336c4640a0048f9f39949c9c2 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 25 May 2015 21:53:04 -0400 Subject: [PATCH 211/569] removed keyword arg from elm327 constructor calls --- obd/obd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/obd.py b/obd/obd.py index 3ef4ff6d..4d057111 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -66,14 +66,14 @@ def __connect(self, portstr, baudrate): for port in portnames: debug("Attempting to use port: " + str(port)) - self.port = ELM327(port, baudrate=baudrate) + self.port = ELM327(port, baudrate) if self.port.is_connected(): # success! stop searching for serial break else: debug("Explicit port defined") - self.port = ELM327(portstr, baudrate=baudrate) + self.port = ELM327(portstr, baudrate) # if a connection was made, query for commands if self.is_connected(): From 1938e9650ba6e96c3ea3aa102d3dc5bf8542defd Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 31 May 2015 17:24:26 -0400 Subject: [PATCH 212/569] using flush(), rather than flushOutput() to prevent truncating the output --- obd/elm327.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/elm327.py b/obd/elm327.py index fa46b181..50c479bf 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -308,7 +308,7 @@ def __write(self, cmd): if self.__port: cmd += "\r\n" # terminate - self.__port.flushOutput() + self.__port.flush() self.__port.flushInput() self.__port.write(cmd.encode()) # turn the string into bytes debug("write: " + repr(cmd)) From c879c70b0e044e18c4852b37d5ddd79f10554b2d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 1 Jun 2015 15:28:31 -0400 Subject: [PATCH 213/569] moved flush() to after the command has been written, to avoid lingering data --- obd/elm327.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 50c479bf..5b1f8a25 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -308,9 +308,9 @@ def __write(self, cmd): if self.__port: cmd += "\r\n" # terminate - self.__port.flush() - self.__port.flushInput() - self.__port.write(cmd.encode()) # turn the string into bytes + self.__port.flushInput() # dump everything in the input buffer + self.__port.write(cmd.encode()) # turn the string into bytes and write + self.__port.flush() # wait for the output buffer to finish transmitting debug("write: " + repr(cmd)) else: debug("cannot perform __write() when unconnected", True) From d664bd8800dc8a3e59429056f68c549a4c388061 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 1 Jun 2015 16:30:02 -0400 Subject: [PATCH 214/569] baudrate is no longer optional in tests --- tests/test_elm327.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_elm327.py b/tests/test_elm327.py index 0264c18f..83c0ed4a 100644 --- a/tests/test_elm327.py +++ b/tests/test_elm327.py @@ -6,7 +6,7 @@ def test_find_primary_ecu(): # parse from messages - p = ELM327("/dev/null") # pyserial will yell, but this isn't testing tx/rx + p = ELM327("/dev/null", 38400) # pyserial will yell, but this isn't testing tx/rx p._ELM327__protocol = SAE_J1850_PWM() # use primary ECU when multiple are present From 0a5b077b59e552ba7e4fe3e0cb987c51bdf4311c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 7 Jun 2015 21:29:31 -0400 Subject: [PATCH 215/569] fixed bug where duplicate Async threads could be started --- obd/async.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/obd/async.py b/obd/async.py index 5cc9a529..3cfdf548 100644 --- a/obd/async.py +++ b/obd/async.py @@ -51,17 +51,22 @@ def __init__(self, portstr=None, baudrate=38400): def start(self): """ Starts the async update loop """ - if self.is_connected(): - if len(self.commands) > 0: - debug("Starting async thread") - self.running = True - self.thread = threading.Thread(target=self.run) - self.thread.daemon = True - self.thread.start() - else: - debug("Async thread not started because no commands were registered") - else: + if not self.is_connected(): debug("Async thread not started because no connection was made") + return + + if len(self.commands) == 0: + debug("Async thread not started because no commands were registered") + return + + if self.thread is None: + debug("Starting async thread") + self.running = True + self.thread = threading.Thread(target=self.run) + self.thread.daemon = True + self.thread.start() + else: + debug("Duplicate start(), async thread was already running") def stop(self): From 86e209d6c16f47d3201ae37bc9a9ffdab9650907 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 9 Jun 2015 22:57:26 -0400 Subject: [PATCH 216/569] added pause() and resume() async helpers --- obd/async.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/obd/async.py b/obd/async.py index 3cfdf548..ac1b7529 100644 --- a/obd/async.py +++ b/obd/async.py @@ -43,10 +43,11 @@ class Async(OBD): def __init__(self, portstr=None, baudrate=38400): super(Async, self).__init__(portstr, baudrate) - self.commands = {} # key = OBDCommand, value = Response - self.callbacks = {} # key = OBDCommand, value = list of Functions - self.thread = None - self.running = False + self.commands = {} # key = OBDCommand, value = Response + self.callbacks = {} # key = OBDCommand, value = list of Functions + self.thread = None + self.running = False + self.was_running = False # used with pause() and resume() def start(self): @@ -61,24 +62,34 @@ def start(self): if self.thread is None: debug("Starting async thread") + self.was_running = False self.running = True self.thread = threading.Thread(target=self.run) self.thread.daemon = True self.thread.start() - else: - debug("Duplicate start(), async thread was already running") def stop(self): """ Stops the async update loop """ if self.thread is not None: debug("Stopping async thread...") + self.was_running = True self.running = False self.thread.join() self.thread = None debug("Async thread stopped") + def pause(self): + self.was_running = self.running + self.stop() + + + def resume(self): + if not self.running and self.was_running: + self.start() + + def close(self): """ Closes the connection """ self.stop() From cd4c6e383999f4628bbb6225db22d0a430c90e3d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 9 Jun 2015 23:23:26 -0400 Subject: [PATCH 217/569] using context manager for pause/resume now --- obd/async.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/obd/async.py b/obd/async.py index ac1b7529..0cd6585b 100644 --- a/obd/async.py +++ b/obd/async.py @@ -62,7 +62,6 @@ def start(self): if self.thread is None: debug("Starting async thread") - self.was_running = False self.running = True self.thread = threading.Thread(target=self.run) self.thread.daemon = True @@ -73,22 +72,43 @@ def stop(self): """ Stops the async update loop """ if self.thread is not None: debug("Stopping async thread...") - self.was_running = True self.running = False self.thread.join() self.thread = None debug("Async thread stopped") - def pause(self): + def paused(self): + """ + A stub function for semantic purposes only + enables code such as: + + with connection.paused() as was_running + ... + """ + return self + + + def __enter__(self): + """ + pauses the async loop, + while recording the old state + """ self.was_running = self.running self.stop() + return self.was_running - def resume(self): + def __exit__(self, exc_type, exc_value, traceback): + """ + resumes the update loop if it was running + when __enter__ was called + """ if not self.running and self.was_running: self.start() + return False # don't suppress any exceptions + def close(self): """ Closes the connection """ From ea902ee366756f9052d72bb9598f8e47181d28f9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 9 Jun 2015 23:27:08 -0400 Subject: [PATCH 218/569] made async properties private --- obd/async.py | 89 +++++++++++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/obd/async.py b/obd/async.py index 0cd6585b..91f730bc 100644 --- a/obd/async.py +++ b/obd/async.py @@ -43,11 +43,16 @@ class Async(OBD): def __init__(self, portstr=None, baudrate=38400): super(Async, self).__init__(portstr, baudrate) - self.commands = {} # key = OBDCommand, value = Response - self.callbacks = {} # key = OBDCommand, value = list of Functions - self.thread = None - self.running = False - self.was_running = False # used with pause() and resume() + self.__commands = {} # key = OBDCommand, value = Response + self.__callbacks = {} # key = OBDCommand, value = list of Functions + self.__thread = None + self.__running = False + self.__was_running = False # used with __enter__() and __exit__() + + + @property + def running(self): + return self.__running def start(self): @@ -56,25 +61,25 @@ def start(self): debug("Async thread not started because no connection was made") return - if len(self.commands) == 0: + if len(self.__commands) == 0: debug("Async thread not started because no commands were registered") return - if self.thread is None: + if self.__thread is None: debug("Starting async thread") - self.running = True - self.thread = threading.Thread(target=self.run) - self.thread.daemon = True - self.thread.start() + self.__running = True + self.__thread = threading.Thread(target=self.run) + self.__thread.daemon = True + self.__thread.start() def stop(self): """ Stops the async update loop """ - if self.thread is not None: + if self.__thread is not None: debug("Stopping async thread...") - self.running = False - self.thread.join() - self.thread = None + self.__running = False + self.__thread.join() + self.__thread = None debug("Async thread stopped") @@ -94,9 +99,9 @@ def __enter__(self): pauses the async loop, while recording the old state """ - self.was_running = self.running + self.__was_running = self.__running self.stop() - return self.was_running + return self.__was_running def __exit__(self, exc_type, exc_value, traceback): @@ -104,7 +109,7 @@ def __exit__(self, exc_type, exc_value, traceback): resumes the update loop if it was running when __enter__ was called """ - if not self.running and self.was_running: + if not self.__running and self.__was_running: self.start() return False # don't suppress any exceptions @@ -124,7 +129,7 @@ def watch(self, c, callback=None, force=False): """ # the dict shouldn't be changed while the daemon thread is iterating - if self.running: + if self.__running: debug("Can't watch() while running, please use stop()", True) else: @@ -133,15 +138,15 @@ def watch(self, c, callback=None, force=False): return # new command being watched, store the command - if c not in self.commands: + if c not in self.__commands: debug("Watching command: %s" % str(c)) - self.commands[c] = Response() # give it an initial value - self.callbacks[c] = [] # create an empty list + self.__commands[c] = Response() # give it an initial value + self.__callbacks[c] = [] # create an empty list # if a callback was given, push it - if hasattr(callback, "__call__") and (callback not in self.callbacks[c]): + if hasattr(callback, "__call__") and (callback not in self.__callbacks[c]): debug("subscribing callback for command: %s" % str(c)) - self.callbacks[c].append(callback) + self.__callbacks[c].append(callback) def unwatch(self, c, callback=None): @@ -152,35 +157,35 @@ def unwatch(self, c, callback=None): """ # the dict shouldn't be changed while the daemon thread is iterating - if self.running: + if self.__running: debug("Can't unwatch() while running, please use stop()", True) else: debug("Unwatching command: %s" % str(c)) - if c in self.commands: + if c in self.__commands: # if a callback was specified, only remove the callback - if hasattr(callback, "__call__") and (callback in self.callbacks[c]): - self.callbacks[c].remove(callback) + if hasattr(callback, "__call__") and (callback in self.__callbacks[c]): + self.__callbacks[c].remove(callback) # if no more callbacks are left, remove the command entirely - if len(self.callbacks[c]) == 0: - self.commands.pop(c, None) + if len(self.__callbacks[c]) == 0: + self.__commands.pop(c, None) else: # no callback was specified, pop everything - self.callbacks.pop(c, None) - self.commands.pop(c, None) + self.__callbacks.pop(c, None) + self.__commands.pop(c, None) def unwatch_all(self): """ Unsubscribes all commands and callbacks from being updated """ # the dict shouldn't be changed while the daemon thread is iterating - if self.running: + if self.__running: debug("Can't unwatch_all() while running, please use stop()", True) else: debug("Unwatching all") - self.commands = {} - self.callbacks = {} + self.__commands = {} + self.__callbacks = {} def query(self, c): @@ -189,8 +194,8 @@ def query(self, c): Only commands that have been watch()ed will return valid responses """ - if c in self.commands: - return self.commands[c] + if c in self.__commands: + return self.__commands[c] else: return Response() @@ -199,20 +204,20 @@ def run(self): """ Daemon thread """ # loop until the stop signal is recieved - while self.running: + while self.__running: - if len(self.commands) > 0: + if len(self.__commands) > 0: # loop over the requested commands, send, and collect the response - for c in self.commands: + for c in self.__commands: # force, since commands are checked for support in watch() r = super(Async, self).query(c, force=True) # store the response - self.commands[c] = r + self.__commands[c] = r # fire the callbacks, if there are any - for callback in self.callbacks[c]: + for callback in self.__callbacks[c]: callback(r) else: From 23b6afccad223cdb530cc1de527542b59e08af46 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 9 Jun 2015 23:50:12 -0400 Subject: [PATCH 219/569] imported docs from github wiki --- docs/Async Querying.md | 73 +++++++++++++++++++ docs/Command Tables.md | 155 ++++++++++++++++++++++++++++++++++++++++ docs/Custom Commands.md | 37 ++++++++++ docs/Debug.md | 14 ++++ docs/Home.md | 19 +++++ docs/OBD Commands.md | 48 +++++++++++++ docs/OBD Connections.md | 55 ++++++++++++++ docs/Query Responses.md | 44 ++++++++++++ 8 files changed, 445 insertions(+) create mode 100644 docs/Async Querying.md create mode 100644 docs/Command Tables.md create mode 100644 docs/Custom Commands.md create mode 100644 docs/Debug.md create mode 100644 docs/Home.md create mode 100644 docs/OBD Commands.md create mode 100644 docs/OBD Connections.md create mode 100644 docs/Query Responses.md diff --git a/docs/Async Querying.md b/docs/Async Querying.md new file mode 100644 index 00000000..18764044 --- /dev/null +++ b/docs/Async Querying.md @@ -0,0 +1,73 @@ +Since the standard `query()` function is blocking, it can be a hazard for UI event loops. To deal with this, python-OBD has an `Async` connection object that can be used in place of the standard `OBD` object. + +`Async` is a subclass of `OBD`, and therefore inherits all of the standard methods. However, `Async` adds a few, in order to manage a list of commands and responses. This way, when the user `query`s the car, the latest response is returned immediately. + +```python +import obd + +connection = obd.Async() # same constructor as 'obd.OBD()' + +connection.watch(obd.commands.RPM) # keep track of the RPM + +connection.start() # start the async update loop + +print connection.query(obd.commands.RPM) # non-blocking, returns immediately +``` + +Callbacks can also be specified, and will return new `Response`s when available. + +```python +import obd +import time + +connection = obd.Async() # same constructor as 'obd.OBD()' + +# a callback that prints every new value to the console +def new_rpm(r): + print r.value + +connection.watch(obd.commands.RPM, callback=new_rpm) +connection.start() + +# the callback will now be fired upon receipt of new values + +time.sleep(60) +connection.stop() +``` + + +## Methods + +##### Async.start() + +Starts the update loop. + +- - - + +##### Async.stop() + +Stops the update loop. + +- - - + +##### Async.watch(command, callback=None, force=False) + +*Note: The async loop must be stopped before this function can be called* + +Subscribes a command to be continuously updated. After calling `watch()`, the `query()` function will return the latest `Response` from that command. An optional callback can also be set, and will be fired upon receipt of new values. Multiple callbacks for the same command are welcome. An optional `force` parameter will force an unsupported command to be sent. + +- - - + +##### Async.unwatch(command, callback=None) + +*Note: The async loop must be stopped before this function can be called* + +Unsubscribes a command from being updated. If no callback is specified, all callbacks for that command are dropped. If a callback is given, only that callback is unsubscribed (all others remain live). + +- - - + +##### Async.unwatch_all() + +*Note: The async loop must be stopped before this function can be called* + +Unsubscribes all commands and callbacks. diff --git a/docs/Command Tables.md b/docs/Command Tables.md new file mode 100644 index 00000000..9aff1df5 --- /dev/null +++ b/docs/Command Tables.md @@ -0,0 +1,155 @@ +Mode 01 +------- + +|PID | Name | Description | +|----|---------------------------|-----------------------------------------| +| 00 | PIDS_A | Supported PIDs [01-20] | +| 01 | STATUS | Status since DTCs cleared | +| 02 | \ | \ | +| 03 | FUEL_STATUS | Fuel System Status | +| 04 | ENGINE_LOAD | Calculated Engine Load | +| 05 | COOLANT_TEMP | Engine Coolant Temperature | +| 06 | SHORT_FUEL_TRIM_1 | Short Term Fuel Trim - Bank 1 | +| 07 | LONG_FUEL_TRIM_1 | Long Term Fuel Trim - Bank 1 | +| 08 | SHORT_FUEL_TRIM_2 | Short Term Fuel Trim - Bank 2 | +| 09 | LONG_FUEL_TRIM_2 | Long Term Fuel Trim - Bank 2 | +| 0A | FUEL_PRESSURE | Fuel Pressure | +| 0B | INTAKE_PRESSURE | Intake Manifold Pressure | +| 0C | RPM | Engine RPM | +| 0D | SPEED | Vehicle Speed | +| 0E | TIMING_ADVANCE | Timing Advance | +| 0F | INTAKE_TEMP | Intake Air Temp | +| 10 | MAF | Air Flow Rate (MAF) | +| 11 | THROTTLE_POS | Throttle Position | +| 12 | AIR_STATUS | Secondary Air Status | +| 13 | \ | \ | +| 14 | O2_B1S1 | O2: Bank 1 - Sensor 1 Voltage | +| 15 | O2_B1S2 | O2: Bank 1 - Sensor 2 Voltage | +| 16 | O2_B1S3 | O2: Bank 1 - Sensor 3 Voltage | +| 17 | O2_B1S4 | O2: Bank 1 - Sensor 4 Voltage | +| 18 | O2_B2S1 | O2: Bank 2 - Sensor 1 Voltage | +| 19 | O2_B2S2 | O2: Bank 2 - Sensor 2 Voltage | +| 1A | O2_B2S3 | O2: Bank 2 - Sensor 3 Voltage | +| 1B | O2_B2S4 | O2: Bank 2 - Sensor 4 Voltage | +| 1C | OBD_COMPLIANCE | OBD Standards Compliance | +| 1D | \ | \ | +| 1E | \ | \ | +| 1F | RUN_TIME | Engine Run Time | +| 20 | PIDS_B | Supported PIDs [21-40] | +| 21 | DISTANCE_W_MIL | Distance Traveled with MIL on | +| 22 | FUEL_RAIL_PRESSURE_VAC | Fuel Rail Pressure (relative to vacuum) | +| 23 | FUEL_RAIL_PRESSURE_DIRECT | Fuel Rail Pressure (direct inject) | +| 24 | O2_S1_WR_VOLTAGE | 02 Sensor 1 WR Lambda Voltage | +| 25 | O2_S2_WR_VOLTAGE | 02 Sensor 2 WR Lambda Voltage | +| 26 | O2_S3_WR_VOLTAGE | 02 Sensor 3 WR Lambda Voltage | +| 27 | O2_S4_WR_VOLTAGE | 02 Sensor 4 WR Lambda Voltage | +| 28 | O2_S5_WR_VOLTAGE | 02 Sensor 5 WR Lambda Voltage | +| 29 | O2_S6_WR_VOLTAGE | 02 Sensor 6 WR Lambda Voltage | +| 2A | O2_S7_WR_VOLTAGE | 02 Sensor 7 WR Lambda Voltage | +| 2B | O2_S8_WR_VOLTAGE | 02 Sensor 8 WR Lambda Voltage | +| 2C | COMMANDED_EGR | Commanded EGR | +| 2D | EGR_ERROR | EGR Error | +| 2E | EVAPORATIVE_PURGE | Commanded Evaporative Purge | +| 2F | FUEL_LEVEL | Fuel Level Input | +| 30 | WARMUPS_SINCE_DTC_CLEAR | Number of warm-ups since codes cleared | +| 31 | DISTANCE_SINCE_DTC_CLEAR | Distance traveled since codes cleared | +| 32 | EVAP_VAPOR_PRESSURE | Evaporative system vapor pressure | +| 33 | BAROMETRIC_PRESSURE | Barometric Pressure | +| 34 | O2_S1_WR_CURRENT | 02 Sensor 1 WR Lambda Current | +| 35 | O2_S2_WR_CURRENT | 02 Sensor 2 WR Lambda Current | +| 36 | O2_S3_WR_CURRENT | 02 Sensor 3 WR Lambda Current | +| 37 | O2_S4_WR_CURRENT | 02 Sensor 4 WR Lambda Current | +| 38 | O2_S5_WR_CURRENT | 02 Sensor 5 WR Lambda Current | +| 39 | O2_S6_WR_CURRENT | 02 Sensor 6 WR Lambda Current | +| 3A | O2_S7_WR_CURRENT | 02 Sensor 7 WR Lambda Current | +| 3B | O2_S8_WR_CURRENT | 02 Sensor 8 WR Lambda Current | +| 3C | CATALYST_TEMP_B1S1 | Catalyst Temperature: Bank 1 - Sensor 1 | +| 3D | CATALYST_TEMP_B2S1 | Catalyst Temperature: Bank 2 - Sensor 1 | +| 3E | CATALYST_TEMP_B1S2 | Catalyst Temperature: Bank 1 - Sensor 2 | +| 3F | CATALYST_TEMP_B2S2 | Catalyst Temperature: Bank 2 - Sensor 2 | +| 40 | PIDS_C | Supported PIDs [41-60] | +| 41 | \ | \ | +| 42 | \ | \ | +| 43 | \ | \ | +| 44 | \ | \ | +| 45 | RELATIVE_THROTTLE_POS | Relative throttle position | +| 46 | AMBIANT_AIR_TEMP | Ambient air temperature | +| 47 | THROTTLE_POS_B | Absolute throttle position B | +| 48 | THROTTLE_POS_C | Absolute throttle position C | +| 49 | ACCELERATOR_POS_D | Accelerator pedal position D | +| 4A | ACCELERATOR_POS_E | Accelerator pedal position E | +| 4B | ACCELERATOR_POS_F | Accelerator pedal position F | +| 4C | THROTTLE_ACTUATOR | Commanded throttle actuator | +| 4D | RUN_TIME_MIL | Time run with MIL on | +| 4E | TIME_SINCE_DTC_CLEARED | Time since trouble codes cleared | +| 4F | \ | \ | +| 50 | MAX_MAF | Maximum value for mass air flow sensor | +| 51 | FUEL_TYPE | Fuel Type | +| 52 | ETHANOL_PERCENT | Ethanol Fuel Percent | +| 53 | EVAP_VAPOR_PRESSURE_ABS | Absolute Evap system Vapor Pressure | +| 54 | EVAP_VAPOR_PRESSURE_ALT | Evap system vapor pressure | +| 55 | SHORT_O2_TRIM_B1 | Short term secondary O2 trim - Bank 1 | +| 56 | LONG_O2_TRIM_B1 | Long term secondary O2 trim - Bank 1 | +| 57 | SHORT_O2_TRIM_B2 | Short term secondary O2 trim - Bank 2 | +| 58 | LONG_O2_TRIM_B2 | Long term secondary O2 trim - Bank 2 | +| 59 | FUEL_RAIL_PRESSURE_ABS | Fuel rail pressure (absolute) | +| 5A | RELATIVE_ACCEL_POS | Relative accelerator pedal position | +| 5B | HYBRID_BATTERY_REMAINING | Hybrid battery pack remaining life | +| 5C | OIL_TEMP | Engine oil temperature | +| 5D | FUEL_INJECT_TIMING | Fuel injection timing | +| 5E | FUEL_RATE | Engine fuel rate | +| 5F | \ | \ | + +Mode 02 +------- +Mode 02 commands are the same as mode 01, but are metrics from when the last DTC occurred (the freeze frame). To access them by name, simple prepend `DTC_` to the Mode 01 command name. + +```python +import obd + +obd.commands.RPM # the Mode 01 command +# vs. +obd.commands.DTC_RPM # the Mode 02 command +``` + +Mode 03 +------- +Mode 03 contains a single command `GET_DTC` which requests all diagnostic trouble codes from the vehicle's engine. + +|PID | Name | Description | +|-----|---------|-----------------------------------------| +| N/A | GET_DTC | Get Diagnostic Trouble Codes | + +This command requests all diagnostic trouble codes from the vehicle's engine. The `value` field of the response object will contain a list of tuples, where each tuple contains the DTC, and a string description of that DTC (if available). + +```python +import obd +connection = obd.OBD() +r = connection.query(obd.commands.GET_DTC) +print(r.value) + +''' +example output: +[ + ("P0030", "HO2S Heater Control Circuit"), + ("P1367", "Unknown error code") +] +''' +``` + +Mode 04 +------- + +|PID | Name | Description | +|-----|-----------|-----------------------------------------| +| N/A | CLEAR_DTC | Clear DTCs and Freeze data | + + +Mode 07 +------- + +The return value will be encoded in the same structure as the Mode 03 `GET_DTC` command. + +|PID | Name | Description | +|-----|----------------|------------------------------| +| N/A | GET_FREEZE_DTC | Get Freeze DTCs | diff --git a/docs/Custom Commands.md b/docs/Custom Commands.md new file mode 100644 index 00000000..5fd95eed --- /dev/null +++ b/docs/Custom Commands.md @@ -0,0 +1,37 @@ +If the command you need is not in python-OBDs tables, you can create a new `OBDCommand` object. The constructor accepts the following arguments (each will become a property). + +| Argument | Type | Description | +|----------------------|----------|--------------------------------------------------------------------------| +| name | string | (human readability only) | +| desc | string | (human readability only) | +| mode | string | OBD mode (hex) | +| pid | string | OBD PID (hex) | +| bytes | int | Number of bytes expected in response | +| decoder | callable | Function used for decoding the hex response | +| supported (optional) | bool | Flag to prevent the sending of unsupported commands (`False` by default) | + +*When the command is sent, the `mode` and `pid` properties are simply concatenated. For unusual codes that don't follow the `mode + pid` structure, feel free to use just one, while setting the other to an empty string.* + +The `decoder` argument is a function of following form. + +```python + def (_hex): + ... + return (, ) +``` + +The `_hex` argument is the data recieved from the car, and is guaranteed to be the size of the `bytes` property specified in the OBDCommand. + +For example: + +```python +from obd import OBDCommand +from obd.utils import unhex + +def rpm(_hex): + v = unhex(_hex) # helper function to convert hex to int + v = v / 4.0 + return (v, obd.Unit.RPM) + +c = OBDCommand("RPM", "Engine RPM", "01", "0C", 2, rpm) +``` diff --git a/docs/Debug.md b/docs/Debug.md new file mode 100644 index 00000000..34ff1faf --- /dev/null +++ b/docs/Debug.md @@ -0,0 +1,14 @@ +python-OBD also contains a debug object that receives status messages and errors. Console printing is disabled by default, but can be enabled manually. A custom debug handler can also be set. + +```python +import obd + +obd.debug.console = True + +# AND / OR + +def log(msg): + print msg + +obd.debug.handler = log +``` diff --git a/docs/Home.md b/docs/Home.md new file mode 100644 index 00000000..27f0e93b --- /dev/null +++ b/docs/Home.md @@ -0,0 +1,19 @@ +Installation +------------ + +Run the following command to download/install the latest release from pypi: + + $ pip install obd + +If you are using a bluetooth adapter, you will need to install the following packages: + + $ sudo apt-get install bluetooth bluez-utils blueman + +Dependencies +------------ ++ pySerial ++ OBD-II adapter (ELM327 Bluetooth Adapter or ELM327 USB Cable) + +Usage +----- +Choose a link from the sidebar to get started diff --git a/docs/OBD Commands.md b/docs/OBD Commands.md new file mode 100644 index 00000000..115a7596 --- /dev/null +++ b/docs/OBD Commands.md @@ -0,0 +1,48 @@ +An `OBDCommand` in python-OBD is an object used to query information from the vehicle. They contain all of the information neccessary to perform the query, and decode the cars response. Python-OBD has built in tables for the most common commands. They can be looked up by name, or by mode/PID (for a full list, see [Command Tables](https://github.com/brendanwhitfield/python-OBD/wiki/Command-Tables)). + +```python +import obd + +c = obd.commands.RPM + +# OR + +c = obd.commands['RPM'] + +# OR + +c = obd.commands[1][12] # mode 1, PID 12 (RPM) +``` + +## Methods + +##### Commands.has_command(command): + +Checks the internal command tables for the existance of the given `OBDCommand` object. Commands are compared by mode and PID value. + +- - - + +##### Commands.has_name(name): + +Checks the internal command tables for a command with the given name. This is also the function of the `in` operator. + +```python +import obd + +obd.commands.has_name('RPM') # True + +# OR + +'RPM' in obd.commands # True +``` + +- - - + +##### Commands.has_pid(mode, pid): + +Checks the internal command tables for a command with the given mode and PID. + +```python +import obd +obd.commands.has_pid(1, 12) # True +``` diff --git a/docs/OBD Connections.md b/docs/OBD Connections.md new file mode 100644 index 00000000..e8cdedca --- /dev/null +++ b/docs/OBD Connections.md @@ -0,0 +1,55 @@ +After installing the library, simply `import obd`, and create a new OBD connection object. By default, python-OBD will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the scanSerial helper retrieve a list of connected ports. + +```python +import obd + +connection = obd.OBD() # auto connect + +# OR + +connection = obd.OBD("/dev/ttyUSB0") # create connection with USB 0 + +# OR + +ports = obd.scanSerial() # return list of valid USB or RF ports +print ports # ['/dev/ttyUSB0', '/dev/ttyUSB1'] +connection = obd.OBD(ports[0]) # connect to the first port in the list +``` + +## Methods + +##### OBD.query(command, force=False) + +Sends an `OBDCommand` to the car, and returns a `Response` object. This function will block until a response is recieved from the car. This function will also check whether the given command is supported by your car. If a command is not marked as supported, it will not be sent to the car, and an empty `Response` will be returned. To force an unsupported command to be sent, there is an optional `force` parameter for your convenience. + +*For non-blocking querying, see [Async Querying](https://github.com/brendanwhitfield/python-OBD/wiki/Async-Querying)* + +- - - + +##### OBD.is_connected() + +Returns a boolean for whether a connection was established. + +- - - + +##### OBD.get_port_name() + +Returns the string name for the currently connected port (`"/dev/ttyUSB0"`). If no connection was made, this function returns `"Not connected to any port"`. + +- - - + +##### OBD.supports(command) + +Returns a boolean for whether a command is supported by both the car and python-OBD + +- - - + +##### OBD.close() + +Closes the connection. + +## Properties + +##### OBD.supported_commands + +A list of commands supported by the car. diff --git a/docs/Query Responses.md b/docs/Query Responses.md new file mode 100644 index 00000000..92943da6 --- /dev/null +++ b/docs/Query Responses.md @@ -0,0 +1,44 @@ +The `query()` function returns `Response` objects. These objects have the following properties: + +| Property | Description | +|----------|------------------------------------------------------------------------| +| value | The decoded value from the car | +| unit | The units of the decoded value | +| command | The `OBDCommand` object that triggered this response | +| message | The internal `Message` object containing the raw response from the car | +| time | Timestamp of response (as given by [`time.time()`](https://docs.python.org/2/library/time.html#time.time)) | + +The `value` property typically contains numeric values, but can also hold complex structures (depending upon the command being decoded). + +If python-OBD is unable to retrieve a response from the car, an empty `Response` object will be returned. Use `is_null()` to check for empty responses. + +## Units + +Unit values can be found in the `Unit` class (enum). + +```python +from obd.utils import Unit +``` + +| Name | Value | +|-------------|--------------------| +| NONE | None | +| RATIO | "Ratio" | +| COUNT | "Count" | +| PERCENT | "%" | +| RPM | "RPM" | +| VOLT | "Volt" | +| F | "F" | +| C | "C" | +| SEC | "Second" | +| MIN | "Minute" | +| PA | "Pa" | +| KPA | "kPa" | +| PSI | "psi" | +| KPH | "kph" | +| MPH | "mph" | +| DEGREES | "Degrees" | +| GPS | "Grams per Second" | +| MA | "mA" | +| KM | "km" | +| LPH | "Liters per Hour" | From 1d9a3f058f25a643af7374d79036b8b1f33efa5a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 10 Jun 2015 15:34:09 -0400 Subject: [PATCH 220/569] updated the async docs for context manager pausing --- docs/Async Querying.md | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/docs/Async Querying.md b/docs/Async Querying.md index 18764044..388cd0f5 100644 --- a/docs/Async Querying.md +++ b/docs/Async Querying.md @@ -1,6 +1,6 @@ -Since the standard `query()` function is blocking, it can be a hazard for UI event loops. To deal with this, python-OBD has an `Async` connection object that can be used in place of the standard `OBD` object. +Since the standard `query()` function is blocking, it can be a hazard for UI event loops. To deal with this, python-OBD has an `Async` connection object that can be used in place of the standard `OBD` object. `Async` is a subclass of `OBD`, and therefore inherits all of the standard methods. However, `Async` adds a few in order to control a threaded update loop. This loop will keep the values of your commands up to date with the vehicle. This way, when the user `query`s the car, the latest response is returned immediately. -`Async` is a subclass of `OBD`, and therefore inherits all of the standard methods. However, `Async` adds a few, in order to manage a list of commands and responses. This way, when the user `query`s the car, the latest response is returned immediately. +The update loop is controlled by calling `start()` and `stop()`. To subscribe a command for updating, call `watch()` with your requested OBDCommand. Because the update loop is threaded, commands can only be `watch`ed while the loop is `stop`ed. ```python import obd @@ -14,13 +14,13 @@ connection.start() # start the async update loop print connection.query(obd.commands.RPM) # non-blocking, returns immediately ``` -Callbacks can also be specified, and will return new `Response`s when available. +Callbacks can also be specified in `watch()`, and will return new `Response`s when available. ```python import obd import time -connection = obd.Async() # same constructor as 'obd.OBD()' +connection = obd.Async() # a callback that prints every new value to the console def new_rpm(r): @@ -50,9 +50,33 @@ Stops the update loop. - - - +##### Async.paused(): + +A helper function for use in a Context Manager (a `with` statement) to temporarily stop the update loop. This makes it easy to protect your `watch()` and `unwatch()` calls. If the update loop was running at the time of being paused, it will be restarted upon exitting the context block. For instance: + +```python +with connection.paused() as was_running: + # connection is stopped within this block + # your code here +``` + +The code above is equivalent to: + +```python +was_running = connection.running +connection.stop() + +# your code here + +if was_running: + connection.start() +``` + +- - - + ##### Async.watch(command, callback=None, force=False) -*Note: The async loop must be stopped before this function can be called* +*Note: The async loop must be stopped or paused before this function can be called* Subscribes a command to be continuously updated. After calling `watch()`, the `query()` function will return the latest `Response` from that command. An optional callback can also be set, and will be fired upon receipt of new values. Multiple callbacks for the same command are welcome. An optional `force` parameter will force an unsupported command to be sent. @@ -60,7 +84,7 @@ Subscribes a command to be continuously updated. After calling `watch()`, the `q ##### Async.unwatch(command, callback=None) -*Note: The async loop must be stopped before this function can be called* +*Note: The async loop must be stopped or paused before this function can be called* Unsubscribes a command from being updated. If no callback is specified, all callbacks for that command are dropped. If a callback is given, only that callback is unsubscribed (all others remain live). @@ -68,6 +92,6 @@ Unsubscribes a command from being updated. If no callback is specified, all call ##### Async.unwatch_all() -*Note: The async loop must be stopped before this function can be called* +*Note: The async loop must be stopped or paused before this function can be called* Unsubscribes all commands and callbacks. From e00d4af2e6cfddb96e3c5b2fdea4b23022f2fea5 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 10 Jun 2015 20:48:28 -0400 Subject: [PATCH 221/569] started using mkdocs --- docs/Custom Commands.md | 3 +++ docs/OBD Commands.md | 20 +++++++++++++------- docs/index.md | 17 +++++++++++++++++ mkdocs.yml | 8 ++++++++ 4 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 docs/index.md create mode 100644 mkdocs.yml diff --git a/docs/Custom Commands.md b/docs/Custom Commands.md index 5fd95eed..f9a536c4 100644 --- a/docs/Custom Commands.md +++ b/docs/Custom Commands.md @@ -1,3 +1,6 @@ +Custom OBD Commands +=================== + If the command you need is not in python-OBDs tables, you can create a new `OBDCommand` object. The constructor accepts the following arguments (each will become a property). | Argument | Type | Description | diff --git a/docs/OBD Commands.md b/docs/OBD Commands.md index 115a7596..e0f40ac1 100644 --- a/docs/OBD Commands.md +++ b/docs/OBD Commands.md @@ -1,4 +1,8 @@ -An `OBDCommand` in python-OBD is an object used to query information from the vehicle. They contain all of the information neccessary to perform the query, and decode the cars response. Python-OBD has built in tables for the most common commands. They can be looked up by name, or by mode/PID (for a full list, see [Command Tables](https://github.com/brendanwhitfield/python-OBD/wiki/Command-Tables)). +# OBD Commands + +--- + +An `OBDCommand` is an object used to query information from the vehicle. They contain all of the information neccessary to perform the query, and decode the cars response. Python-OBD has built in tables for the most common commands. They can be looked up by name, or by mode/PID (for a full list, see [Command Tables](https://github.com/brendanwhitfield/python-OBD/wiki/Command-Tables)). ```python import obd @@ -14,15 +18,17 @@ c = obd.commands['RPM'] c = obd.commands[1][12] # mode 1, PID 12 (RPM) ``` -## Methods +# Methods + +--- -##### Commands.has_command(command): +## has_command(command) Checks the internal command tables for the existance of the given `OBDCommand` object. Commands are compared by mode and PID value. -- - - +--- -##### Commands.has_name(name): +## has_name(name) Checks the internal command tables for a command with the given name. This is also the function of the `in` operator. @@ -36,9 +42,9 @@ obd.commands.has_name('RPM') # True 'RPM' in obd.commands # True ``` -- - - +--- -##### Commands.has_pid(mode, pid): +## has_pid(mode, pid) Checks the internal command tables for a command with the given mode and PID. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..da37213a --- /dev/null +++ b/docs/index.md @@ -0,0 +1,17 @@ +# Welcome to MkDocs + +For full documentation visit [mkdocs.org](http://mkdocs.org). + +## Commands + +* `mkdocs new [dir-name]` - Create a new project. +* `mkdocs serve` - Start the live-reloading docs server. +* `mkdocs build` - Build the documentation site. +* `mkdocs help` - Print this help message. + +## Project layout + + mkdocs.yml # The configuration file. + docs/ + index.md # The documentation homepage. + ... # Other markdown pages, images and other files. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..77b303ed --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,8 @@ +site_name: python-OBD +repo_url: https://github.com/brendanwhitfield/python-OBD +repo_name: GitHub +pages: +- Home: 'index.md' +- Commands: + - 'OBD Commands': 'OBD Commands.md' + - 'Custom Commands': 'Custom Commands.md' From eb155736dd995056e5d242078aa6b1efd2d11429 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 10 Jun 2015 22:09:11 -0400 Subject: [PATCH 222/569] started reformatting docs for rtfd --- docs/Async Connections.md | 102 ++++++++++++++++++++++++++++++++++ docs/Command Table Methods.md | 40 +++++++++++++ docs/Command Tables.md | 69 +++++++++++++++++++---- docs/Custom Commands.md | 6 +- docs/Debug.md | 4 ++ docs/OBD Commands.md | 52 +---------------- docs/OBD Connections.md | 41 +++++++++----- docs/OBD Responses.md | 48 ++++++++++++++++ docs/index.md | 27 +++++---- mkdocs.yml | 13 ++++- 10 files changed, 311 insertions(+), 91 deletions(-) create mode 100644 docs/Async Connections.md create mode 100644 docs/Command Table Methods.md create mode 100644 docs/OBD Responses.md diff --git a/docs/Async Connections.md b/docs/Async Connections.md new file mode 100644 index 00000000..4fcc402e --- /dev/null +++ b/docs/Async Connections.md @@ -0,0 +1,102 @@ +Since the standard `query()` function is blocking, it can be a hazard for UI event loops. To deal with this, python-OBD has an `Async` connection object that can be used in place of the standard `OBD` object. `Async` is a subclass of `OBD`, and therefore inherits all of the standard methods. However, `Async` adds a few in order to control a threaded update loop. This loop will keep the values of your commands up to date with the vehicle. This way, when the user `query`s the car, the latest response is returned immediately. + +The update loop is controlled by calling `start()` and `stop()`. To subscribe a command for updating, call `watch()` with your requested OBDCommand. Because the update loop is threaded, commands can only be `watch`ed while the loop is `stop`ed. + +```python +import obd + +connection = obd.Async() # same constructor as 'obd.OBD()' + +connection.watch(obd.commands.RPM) # keep track of the RPM + +connection.start() # start the async update loop + +print connection.query(obd.commands.RPM) # non-blocking, returns immediately +``` + +Callbacks can also be specified in `watch()`, and will return new `Response`s when available. + +```python +import obd +import time + +connection = obd.Async() + +# a callback that prints every new value to the console +def new_rpm(r): + print r.value + +connection.watch(obd.commands.RPM, callback=new_rpm) +connection.start() + +# the callback will now be fired upon receipt of new values + +time.sleep(60) +connection.stop() +``` + +
+ +--- + +## start() + +Starts the update loop. + +--- + +## stop() + +Stops the update loop. + +--- + +## paused() + +A helper function for use in a Context Manager (a `with` statement) to temporarily stop the update loop. This makes it easy to protect your `watch()` and `unwatch()` calls. If the update loop was running at the time of being paused, it will be restarted upon exitting the context block. For instance: + +```python +with connection.paused() as was_running: + # connection is stopped within this block + # your code here +``` + +The code above is equivalent to: + +```python +was_running = connection.running +connection.stop() + +# your code here + +if was_running: + connection.start() +``` + +--- + +## watch(command, callback=None, force=False) + +*Note: The async loop must be stopped or paused before this function can be called* + +Subscribes a command to be continuously updated. After calling `watch()`, the `query()` function will return the latest `Response` from that command. An optional callback can also be set, and will be fired upon receipt of new values. Multiple callbacks for the same command are welcome. An optional `force` parameter will force an unsupported command to be sent. + +--- + +## unwatch(command, callback=None) + +*Note: The async loop must be stopped or paused before this function can be called* + +Unsubscribes a command from being updated. If no callback is specified, all callbacks for that command are dropped. If a callback is given, only that callback is unsubscribed (all others remain live). + +--- + +## unwatch_all() + +*Note: The async loop must be stopped or paused before this function can be called* + +Unsubscribes all commands and callbacks. + +--- + +
diff --git a/docs/Command Table Methods.md b/docs/Command Table Methods.md new file mode 100644 index 00000000..497dddd4 --- /dev/null +++ b/docs/Command Table Methods.md @@ -0,0 +1,40 @@ + +## has_command(command) + +Checks the internal command tables for the existance of the given `OBDCommand` object. Commands are compared by mode and PID value. + +```python +import obd +obd.commands.has_command(obd.commands.RPM) # True +``` + +--- + +## has_name(name) + +Checks the internal command tables for a command with the given name. This is also the function of the `in` operator. + +```python +import obd + +obd.commands.has_name('RPM') # True + +# OR + +'RPM' in obd.commands # True +``` + +--- + +## has_pid(mode, pid) + +Checks the internal command tables for a command with the given mode and PID. + +```python +import obd +obd.commands.has_pid(1, 12) # True +``` + +--- + +
diff --git a/docs/Command Tables.md b/docs/Command Tables.md index 9aff1df5..bb9557f8 100644 --- a/docs/Command Tables.md +++ b/docs/Command Tables.md @@ -1,5 +1,27 @@ -Mode 01 -------- + +Python-OBD has built in tables for the most common commands. They can be looked up by name, or by mode/PID (for a full list, see [Command Tables](https://github.com/brendanwhitfield/python-OBD/wiki/Command-Tables)). + +```python +import obd + +c = obd.commands.RPM + +# OR + +c = obd.commands['RPM'] + +# OR + +c = obd.commands[1][12] # mode 1, PID 12 (RPM) +``` + +
+ +--- + +
+ +# Mode 01 |PID | Name | Description | |----|---------------------------|-----------------------------------------| @@ -100,8 +122,14 @@ Mode 01 | 5E | FUEL_RATE | Engine fuel rate | | 5F | \ | \ | -Mode 02 -------- +
+ +--- + +
+ +# Mode 02 + Mode 02 commands are the same as mode 01, but are metrics from when the last DTC occurred (the freeze frame). To access them by name, simple prepend `DTC_` to the Mode 01 command name. ```python @@ -112,8 +140,14 @@ obd.commands.RPM # the Mode 01 command obd.commands.DTC_RPM # the Mode 02 command ``` -Mode 03 -------- +
+ +--- + +
+ +# Mode 03 + Mode 03 contains a single command `GET_DTC` which requests all diagnostic trouble codes from the vehicle's engine. |PID | Name | Description | @@ -137,19 +171,34 @@ example output: ''' ``` -Mode 04 -------- +
+ +--- + +
+ +# Mode 04 |PID | Name | Description | |-----|-----------|-----------------------------------------| | N/A | CLEAR_DTC | Clear DTCs and Freeze data | +
+ +--- + +
-Mode 07 -------- +# Mode 07 The return value will be encoded in the same structure as the Mode 03 `GET_DTC` command. |PID | Name | Description | |-----|----------------|------------------------------| | N/A | GET_FREEZE_DTC | Get Freeze DTCs | + +
+ +--- + +
diff --git a/docs/Custom Commands.md b/docs/Custom Commands.md index f9a536c4..a5952138 100644 --- a/docs/Custom Commands.md +++ b/docs/Custom Commands.md @@ -1,5 +1,3 @@ -Custom OBD Commands -=================== If the command you need is not in python-OBDs tables, you can create a new `OBDCommand` object. The constructor accepts the following arguments (each will become a property). @@ -38,3 +36,7 @@ def rpm(_hex): c = OBDCommand("RPM", "Engine RPM", "01", "0C", 2, rpm) ``` + +--- + +
diff --git a/docs/Debug.md b/docs/Debug.md index 34ff1faf..0cacdb7c 100644 --- a/docs/Debug.md +++ b/docs/Debug.md @@ -12,3 +12,7 @@ def log(msg): obd.debug.handler = log ``` + +--- + +
diff --git a/docs/OBD Commands.md b/docs/OBD Commands.md index e0f40ac1..6de4059c 100644 --- a/docs/OBD Commands.md +++ b/docs/OBD Commands.md @@ -1,54 +1,6 @@ -# OBD Commands ---- - -An `OBDCommand` is an object used to query information from the vehicle. They contain all of the information neccessary to perform the query, and decode the cars response. Python-OBD has built in tables for the most common commands. They can be looked up by name, or by mode/PID (for a full list, see [Command Tables](https://github.com/brendanwhitfield/python-OBD/wiki/Command-Tables)). - -```python -import obd - -c = obd.commands.RPM - -# OR - -c = obd.commands['RPM'] - -# OR - -c = obd.commands[1][12] # mode 1, PID 12 (RPM) -``` - -# Methods +An `OBDCommand` is an object used to query information from the vehicle. They contain all of the information neccessary to perform the query, and decode the cars response. --- -## has_command(command) - -Checks the internal command tables for the existance of the given `OBDCommand` object. Commands are compared by mode and PID value. - ---- - -## has_name(name) - -Checks the internal command tables for a command with the given name. This is also the function of the `in` operator. - -```python -import obd - -obd.commands.has_name('RPM') # True - -# OR - -'RPM' in obd.commands # True -``` - ---- - -## has_pid(mode, pid) - -Checks the internal command tables for a command with the given mode and PID. - -```python -import obd -obd.commands.has_pid(1, 12) # True -``` +
diff --git a/docs/OBD Connections.md b/docs/OBD Connections.md index e8cdedca..b1c3747b 100644 --- a/docs/OBD Connections.md +++ b/docs/OBD Connections.md @@ -16,40 +16,53 @@ print ports # ['/dev/ttyUSB0', '/dev/ttyUSB1'] connection = obd.OBD(ports[0]) # connect to the first port in the list ``` -## Methods +
-##### OBD.query(command, force=False) +--- -Sends an `OBDCommand` to the car, and returns a `Response` object. This function will block until a response is recieved from the car. This function will also check whether the given command is supported by your car. If a command is not marked as supported, it will not be sent to the car, and an empty `Response` will be returned. To force an unsupported command to be sent, there is an optional `force` parameter for your convenience. +## query(command, force=False) + +Sends an `OBDCommand` to the car, and returns a `OBDResponse` object. This function will block until a response is recieved from the car. This function will also check whether the given command is supported by your car. If a command is not marked as supported, it will not be sent to the car, and an empty `Response` will be returned. To force an unsupported command to be sent, there is an optional `force` parameter for your convenience. *For non-blocking querying, see [Async Querying](https://github.com/brendanwhitfield/python-OBD/wiki/Async-Querying)* -- - - +```python +import obd +connection = obd.OBD() + +r = connection.query(obd.commands.RPM) # returns the response from the car +``` + +--- -##### OBD.is_connected() +## is_connected() Returns a boolean for whether a connection was established. -- - - +--- -##### OBD.get_port_name() +## get_port_name() Returns the string name for the currently connected port (`"/dev/ttyUSB0"`). If no connection was made, this function returns `"Not connected to any port"`. -- - - +--- -##### OBD.supports(command) +## supports(command) Returns a boolean for whether a command is supported by both the car and python-OBD -- - - +--- -##### OBD.close() +## close() Closes the connection. -## Properties +--- + +## supported_commands + +Property containing a list of commands that are supported by the car. -##### OBD.supported_commands +--- -A list of commands supported by the car. +
diff --git a/docs/OBD Responses.md b/docs/OBD Responses.md new file mode 100644 index 00000000..6d35b884 --- /dev/null +++ b/docs/OBD Responses.md @@ -0,0 +1,48 @@ +The `query()` function returns `OBDResponse` objects. These objects have the following properties: + +| Property | Description | +|----------|------------------------------------------------------------------------| +| value | The decoded value from the car | +| unit | The units of the decoded value | +| command | The `OBDCommand` object that triggered this response | +| message | The internal `Message` object containing the raw response from the car | +| time | Timestamp of response (as given by [`time.time()`](https://docs.python.org/2/library/time.html#time.time)) | + +The `value` property typically contains numeric values, but can also hold complex structures (depending upon the command being decoded). + +If python-OBD is unable to retrieve a response from the car, an empty `OBDResponse` object will be returned. Use `is_null()` to check for empty responses. + +## Units + +Unit values can be found in the `Unit` class (enum). + +```python +from obd.utils import Unit +``` + +| Name | Value | +|-------------|--------------------| +| NONE | None | +| RATIO | "Ratio" | +| COUNT | "Count" | +| PERCENT | "%" | +| RPM | "RPM" | +| VOLT | "Volt" | +| F | "F" | +| C | "C" | +| SEC | "Second" | +| MIN | "Minute" | +| PA | "Pa" | +| KPA | "kPa" | +| PSI | "psi" | +| KPH | "kph" | +| MPH | "mph" | +| DEGREES | "Degrees" | +| GPS | "Grams per Second" | +| MA | "mA" | +| KM | "km" | +| LPH | "Liters per Hour" | + +--- + +
diff --git a/docs/index.md b/docs/index.md index da37213a..0921fe75 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,20 @@ -# Welcome to MkDocs +# Installation -For full documentation visit [mkdocs.org](http://mkdocs.org). +Run the following command to download/install the latest release from pypi: -## Commands + $ pip install obd -* `mkdocs new [dir-name]` - Create a new project. -* `mkdocs serve` - Start the live-reloading docs server. -* `mkdocs build` - Build the documentation site. -* `mkdocs help` - Print this help message. +If you are using a bluetooth adapter, you will need to install the following packages: -## Project layout + $ sudo apt-get install bluetooth bluez-utils blueman - mkdocs.yml # The configuration file. - docs/ - index.md # The documentation homepage. - ... # Other markdown pages, images and other files. +--- + +# Dependencies + ++ pySerial ++ OBD-II adapter (ELM327 Bluetooth Adapter or ELM327 USB Cable) + +--- + +
diff --git a/mkdocs.yml b/mkdocs.yml index 77b303ed..99f6993f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,6 +3,13 @@ repo_url: https://github.com/brendanwhitfield/python-OBD repo_name: GitHub pages: - Home: 'index.md' -- Commands: - - 'OBD Commands': 'OBD Commands.md' - - 'Custom Commands': 'Custom Commands.md' +- OBD Connections: 'OBD Connections.md' +- Commands: 'OBD Commands.md' +- Custom Commands: 'Custom Commands.md' +- Command Tables: 'Command Tables.md' +- Command Table Methods: 'Command Table Methods.md' +- Responses: 'OBD Responses.md' +- Async Connections: 'Async Connections.md' +- Debug: 'Debug.md' + +theme: readthedocs From 55286b0409a99f47b91d918b45843f9e417da225 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 10 Jun 2015 22:11:46 -0400 Subject: [PATCH 223/569] removed old files --- docs/Async Querying.md | 97 ----------------------------------------- docs/Home.md | 19 -------- docs/Query Responses.md | 44 ------------------- 3 files changed, 160 deletions(-) delete mode 100644 docs/Async Querying.md delete mode 100644 docs/Home.md delete mode 100644 docs/Query Responses.md diff --git a/docs/Async Querying.md b/docs/Async Querying.md deleted file mode 100644 index 388cd0f5..00000000 --- a/docs/Async Querying.md +++ /dev/null @@ -1,97 +0,0 @@ -Since the standard `query()` function is blocking, it can be a hazard for UI event loops. To deal with this, python-OBD has an `Async` connection object that can be used in place of the standard `OBD` object. `Async` is a subclass of `OBD`, and therefore inherits all of the standard methods. However, `Async` adds a few in order to control a threaded update loop. This loop will keep the values of your commands up to date with the vehicle. This way, when the user `query`s the car, the latest response is returned immediately. - -The update loop is controlled by calling `start()` and `stop()`. To subscribe a command for updating, call `watch()` with your requested OBDCommand. Because the update loop is threaded, commands can only be `watch`ed while the loop is `stop`ed. - -```python -import obd - -connection = obd.Async() # same constructor as 'obd.OBD()' - -connection.watch(obd.commands.RPM) # keep track of the RPM - -connection.start() # start the async update loop - -print connection.query(obd.commands.RPM) # non-blocking, returns immediately -``` - -Callbacks can also be specified in `watch()`, and will return new `Response`s when available. - -```python -import obd -import time - -connection = obd.Async() - -# a callback that prints every new value to the console -def new_rpm(r): - print r.value - -connection.watch(obd.commands.RPM, callback=new_rpm) -connection.start() - -# the callback will now be fired upon receipt of new values - -time.sleep(60) -connection.stop() -``` - - -## Methods - -##### Async.start() - -Starts the update loop. - -- - - - -##### Async.stop() - -Stops the update loop. - -- - - - -##### Async.paused(): - -A helper function for use in a Context Manager (a `with` statement) to temporarily stop the update loop. This makes it easy to protect your `watch()` and `unwatch()` calls. If the update loop was running at the time of being paused, it will be restarted upon exitting the context block. For instance: - -```python -with connection.paused() as was_running: - # connection is stopped within this block - # your code here -``` - -The code above is equivalent to: - -```python -was_running = connection.running -connection.stop() - -# your code here - -if was_running: - connection.start() -``` - -- - - - -##### Async.watch(command, callback=None, force=False) - -*Note: The async loop must be stopped or paused before this function can be called* - -Subscribes a command to be continuously updated. After calling `watch()`, the `query()` function will return the latest `Response` from that command. An optional callback can also be set, and will be fired upon receipt of new values. Multiple callbacks for the same command are welcome. An optional `force` parameter will force an unsupported command to be sent. - -- - - - -##### Async.unwatch(command, callback=None) - -*Note: The async loop must be stopped or paused before this function can be called* - -Unsubscribes a command from being updated. If no callback is specified, all callbacks for that command are dropped. If a callback is given, only that callback is unsubscribed (all others remain live). - -- - - - -##### Async.unwatch_all() - -*Note: The async loop must be stopped or paused before this function can be called* - -Unsubscribes all commands and callbacks. diff --git a/docs/Home.md b/docs/Home.md deleted file mode 100644 index 27f0e93b..00000000 --- a/docs/Home.md +++ /dev/null @@ -1,19 +0,0 @@ -Installation ------------- - -Run the following command to download/install the latest release from pypi: - - $ pip install obd - -If you are using a bluetooth adapter, you will need to install the following packages: - - $ sudo apt-get install bluetooth bluez-utils blueman - -Dependencies ------------- -+ pySerial -+ OBD-II adapter (ELM327 Bluetooth Adapter or ELM327 USB Cable) - -Usage ------ -Choose a link from the sidebar to get started diff --git a/docs/Query Responses.md b/docs/Query Responses.md deleted file mode 100644 index 92943da6..00000000 --- a/docs/Query Responses.md +++ /dev/null @@ -1,44 +0,0 @@ -The `query()` function returns `Response` objects. These objects have the following properties: - -| Property | Description | -|----------|------------------------------------------------------------------------| -| value | The decoded value from the car | -| unit | The units of the decoded value | -| command | The `OBDCommand` object that triggered this response | -| message | The internal `Message` object containing the raw response from the car | -| time | Timestamp of response (as given by [`time.time()`](https://docs.python.org/2/library/time.html#time.time)) | - -The `value` property typically contains numeric values, but can also hold complex structures (depending upon the command being decoded). - -If python-OBD is unable to retrieve a response from the car, an empty `Response` object will be returned. Use `is_null()` to check for empty responses. - -## Units - -Unit values can be found in the `Unit` class (enum). - -```python -from obd.utils import Unit -``` - -| Name | Value | -|-------------|--------------------| -| NONE | None | -| RATIO | "Ratio" | -| COUNT | "Count" | -| PERCENT | "%" | -| RPM | "RPM" | -| VOLT | "Volt" | -| F | "F" | -| C | "C" | -| SEC | "Second" | -| MIN | "Minute" | -| PA | "Pa" | -| KPA | "kPa" | -| PSI | "psi" | -| KPH | "kph" | -| MPH | "mph" | -| DEGREES | "Degrees" | -| GPS | "Grams per Second" | -| MA | "mA" | -| KM | "km" | -| LPH | "Liters per Hour" | From 8eb31a195938c7dade9b4f548c7d0374796aa4dd Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 18 Jun 2015 21:09:24 -0400 Subject: [PATCH 224/569] added troubleshooting page --- docs/Async Connections.md | 12 ++--- docs/Command Table Methods.md | 40 ----------------- docs/Command Tables.md | 44 +++++++++++++++++- docs/OBD Connections.md | 15 ++++--- docs/Troubleshooting.md | 85 +++++++++++++++++++++++++++++++++++ mkdocs.yml | 4 +- 6 files changed, 143 insertions(+), 57 deletions(-) delete mode 100644 docs/Command Table Methods.md create mode 100644 docs/Troubleshooting.md diff --git a/docs/Async Connections.md b/docs/Async Connections.md index 4fcc402e..b3de1eb3 100644 --- a/docs/Async Connections.md +++ b/docs/Async Connections.md @@ -39,19 +39,19 @@ connection.stop() --- -## start() +### start() Starts the update loop. --- -## stop() +### stop() Stops the update loop. --- -## paused() +### paused() A helper function for use in a Context Manager (a `with` statement) to temporarily stop the update loop. This makes it easy to protect your `watch()` and `unwatch()` calls. If the update loop was running at the time of being paused, it will be restarted upon exitting the context block. For instance: @@ -75,7 +75,7 @@ if was_running: --- -## watch(command, callback=None, force=False) +### watch(command, callback=None, force=False) *Note: The async loop must be stopped or paused before this function can be called* @@ -83,7 +83,7 @@ Subscribes a command to be continuously updated. After calling `watch()`, the `q --- -## unwatch(command, callback=None) +### unwatch(command, callback=None) *Note: The async loop must be stopped or paused before this function can be called* @@ -91,7 +91,7 @@ Unsubscribes a command from being updated. If no callback is specified, all call --- -## unwatch_all() +### unwatch_all() *Note: The async loop must be stopped or paused before this function can be called* diff --git a/docs/Command Table Methods.md b/docs/Command Table Methods.md deleted file mode 100644 index 497dddd4..00000000 --- a/docs/Command Table Methods.md +++ /dev/null @@ -1,40 +0,0 @@ - -## has_command(command) - -Checks the internal command tables for the existance of the given `OBDCommand` object. Commands are compared by mode and PID value. - -```python -import obd -obd.commands.has_command(obd.commands.RPM) # True -``` - ---- - -## has_name(name) - -Checks the internal command tables for a command with the given name. This is also the function of the `in` operator. - -```python -import obd - -obd.commands.has_name('RPM') # True - -# OR - -'RPM' in obd.commands # True -``` - ---- - -## has_pid(mode, pid) - -Checks the internal command tables for a command with the given mode and PID. - -```python -import obd -obd.commands.has_pid(1, 12) # True -``` - ---- - -
diff --git a/docs/Command Tables.md b/docs/Command Tables.md index bb9557f8..36119e61 100644 --- a/docs/Command Tables.md +++ b/docs/Command Tables.md @@ -1,5 +1,7 @@ -Python-OBD has built in tables for the most common commands. They can be looked up by name, or by mode/PID (for a full list, see [Command Tables](https://github.com/brendanwhitfield/python-OBD/wiki/Command-Tables)). +# Lookup + +Python-OBD has built in tables for the most common commands. They can be looked up by name, or by mode & PID. ```python import obd @@ -15,7 +17,45 @@ c = obd.commands['RPM'] c = obd.commands[1][12] # mode 1, PID 12 (RPM) ``` -
+The `commands` table also has a few helper methods for determining if a particular name or PID is present. + +--- + +### has_command(command) + +Checks the internal command tables for the existance of the given `OBDCommand` object. Commands are compared by mode and PID value. + +```python +import obd +obd.commands.has_command(obd.commands.RPM) # True +``` + +--- + +### has_name(name) + +Checks the internal command tables for a command with the given name. This is also the function of the `in` operator. + +```python +import obd + +obd.commands.has_name('RPM') # True + +# OR + +'RPM' in obd.commands # True +``` + +--- + +### has_pid(mode, pid) + +Checks the internal command tables for a command with the given mode and PID. + +```python +import obd +obd.commands.has_pid(1, 12) # True +``` --- diff --git a/docs/OBD Connections.md b/docs/OBD Connections.md index b1c3747b..be55f7ac 100644 --- a/docs/OBD Connections.md +++ b/docs/OBD Connections.md @@ -1,3 +1,4 @@ + After installing the library, simply `import obd`, and create a new OBD connection object. By default, python-OBD will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the scanSerial helper retrieve a list of connected ports. ```python @@ -20,11 +21,11 @@ connection = obd.OBD(ports[0]) # connect to the first port in the list --- -## query(command, force=False) +### query(command, force=False) Sends an `OBDCommand` to the car, and returns a `OBDResponse` object. This function will block until a response is recieved from the car. This function will also check whether the given command is supported by your car. If a command is not marked as supported, it will not be sent to the car, and an empty `Response` will be returned. To force an unsupported command to be sent, there is an optional `force` parameter for your convenience. -*For non-blocking querying, see [Async Querying](https://github.com/brendanwhitfield/python-OBD/wiki/Async-Querying)* +*For non-blocking querying, see [Async Querying](Async Connections.md)* ```python import obd @@ -35,31 +36,31 @@ r = connection.query(obd.commands.RPM) # returns the response from the car --- -## is_connected() +### is_connected() Returns a boolean for whether a connection was established. --- -## get_port_name() +### get_port_name() Returns the string name for the currently connected port (`"/dev/ttyUSB0"`). If no connection was made, this function returns `"Not connected to any port"`. --- -## supports(command) +### supports(command) Returns a boolean for whether a command is supported by both the car and python-OBD --- -## close() +### close() Closes the connection. --- -## supported_commands +### supported_commands Property containing a list of commands that are supported by the car. diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md new file mode 100644 index 00000000..81a3ba04 --- /dev/null +++ b/docs/Troubleshooting.md @@ -0,0 +1,85 @@ + +# Debug Output + +If python-OBD is not working properly, the first thing you should do is enable debug output. The following line enables console printing: + +```python +obd.debug.console = True +``` + +Here are some common logs from python-OBD, and their meanings: + +
+ +### Successful Connection + +```none +[obd] ========================== python-OBD (v0.4.0) ========================== +[obd] Explicit port defined +[obd] Opening serial port '/dev/pts/2' +[obd] Serial port successfully opened on /dev/pts/2 +[obd] write: 'ATZ\r\n' +[obd] wait: 1 seconds +[obd] read: 'ATZ\rELM327 v2.1\r' +[obd] write: 'ATE0\r\n' +[obd] read: 'ATE0\rOK\r' +[obd] write: 'ATH1\r\n' +[obd] read: 'OK\r' +[obd] write: 'ATL0\r\n' +[obd] read: 'OK\r' +[obd] write: 'ATSPA8\r\n' +[obd] read: 'OK\r' +[obd] write: '0100\r\n' +[obd] read: '7E8 06 41 00 FF FF FF FF FC\r' +[obd] write: 'ATDPN\r\n' +[obd] read: 'A8\r' +[obd] Connection successful +[obd] querying for supported PIDs (commands)... +[obd] Sending command: 0100: Supported PIDs [01-20] +[obd] write: '0100\r\n' +[obd] read: '7E8 06 41 00 FF FF FF FF FC\r' +[obd] Sending command: 0120: Supported PIDs [21-40] +[obd] write: '0120\r\n' +[obd] read: '7E8 06 41 20 FF FF FF FF FC\r' +[obd] Sending command: 0140: Supported PIDs [41-60] +[obd] write: '0140\r\n' +[obd] read: '7E8 06 41 40 FF FF FF FE FB\r' +[obd] finished querying with 93 commands supported +[obd] ========================================================================= +``` + +
+ +### Non-responsive ELM + +``` +[obd] ========================== python-OBD (v0.4.0) ========================== +[obd] Explicit port defined +[obd] Opening serial port '/dev/pts/2' +[obd] Serial port successfully opened on /dev/pts/2 +[obd] write: 'ATZ\r\n' +[obd] wait: 1 seconds +[obd] __read() found nothing +[obd] __read() found nothing +[obd] __read() never recieved prompt character +[obd] read: '' +[obd] write: 'ATE0\r\n' +[obd] __read() found nothing +[obd] __read() found nothing +[obd] __read() never recieved prompt character +[obd] read: '' +[obd] Connection Error: +[obd] ATE0 did not return 'OK' +[obd] Failed to connect +[obd] ========================================================================= +``` + +This is likely a problem with the serial connection between the OBD-II adapter and your computer. Make sure that: + +- bluetooth devices have been paired properly +- you are connecting to the right port in `/dev` (or that there is any port at all) +- you have the correct permissions to write to the port + +--- + +
diff --git a/mkdocs.yml b/mkdocs.yml index 99f6993f..43d90b86 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,11 +5,11 @@ pages: - Home: 'index.md' - OBD Connections: 'OBD Connections.md' - Commands: 'OBD Commands.md' -- Custom Commands: 'Custom Commands.md' - Command Tables: 'Command Tables.md' -- Command Table Methods: 'Command Table Methods.md' +- Custom Commands: 'Custom Commands.md' - Responses: 'OBD Responses.md' - Async Connections: 'Async Connections.md' - Debug: 'Debug.md' +- Troubleshooting: 'Troubleshooting.md' theme: readthedocs From 503b48766fc35494a0ddf70cb5d7375ec2873b41 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 18 Jun 2015 21:47:29 -0400 Subject: [PATCH 225/569] changed wording --- docs/Troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index 81a3ba04..5801ab55 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -50,7 +50,7 @@ Here are some common logs from python-OBD, and their meanings:
-### Non-responsive ELM +### Unresponsive ELM ``` [obd] ========================== python-OBD (v0.4.0) ========================== From a702e5ec45050d3ba55aee7f2b58f9fa4e669852 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 19 Jun 2015 12:16:22 -0400 Subject: [PATCH 226/569] added unrespeonsive vehicle docs --- docs/Troubleshooting.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index 5801ab55..cba73af2 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -80,6 +80,47 @@ This is likely a problem with the serial connection between the OBD-II adapter a - you are connecting to the right port in `/dev` (or that there is any port at all) - you have the correct permissions to write to the port +You can use the `scanSerial()` helper function to determine which ports are available for writing. + +```python +import obd + +ports = obd.scanSerial() # return list of valid USB or RF ports +print ports # ['/dev/ttyUSB0', '/dev/ttyUSB1'] +``` + +
+ +### Unresponsive Vehicle + +``` +[obd] ========================== python-OBD (v0.4.0) ========================== +[obd] Explicit port defined +[obd] Opening serial port '/dev/pts/2' +[obd] Serial port successfully opened on /dev/pts/2 +[obd] write: 'ATZ\r\n' +[obd] wait: 1 seconds +[obd] read: 'ATZ\rELM327 v2.1\r' +[obd] write: 'ATE0\r\n' +[obd] read: 'ATE0\rOK\r' +[obd] write: 'ATH1\r\n' +[obd] read: 'OK\r' +[obd] write: 'ATL0\r\n' +[obd] read: 'OK\r' +[obd] write: 'ATSPA8\r\n' +[obd] read: 'OK\r' +[obd] write: '0100\r\n' +[obd] read: 'SEARCHING...\rUNABLE TO CONNECT\r' +[obd] write: 'ATDPN\r\n' +[obd] read: '0\r' +[obd] Connection Error: +[obd] ELM responded with unknown protocol +[obd] Failed to connect +[obd] ========================================================================= +``` + +This is a connection problem between the ELM adapter and your car. Make sure that you car is powered, and that the electrical connection between the adapter and your car's OBD-II port is sound. + ---
From 2c2353dad10b9e205c30d7a7e7e8a68be842d726 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 19 Jun 2015 12:25:38 -0400 Subject: [PATCH 227/569] fixed escaping issue with <> by using italics instead --- docs/Command Tables.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/Command Tables.md b/docs/Command Tables.md index 36119e61..3c258da6 100644 --- a/docs/Command Tables.md +++ b/docs/Command Tables.md @@ -67,7 +67,7 @@ obd.commands.has_pid(1, 12) # True |----|---------------------------|-----------------------------------------| | 00 | PIDS_A | Supported PIDs [01-20] | | 01 | STATUS | Status since DTCs cleared | -| 02 | \ | \ | +| 02 | *unsupported* | *unsupported* | | 03 | FUEL_STATUS | Fuel System Status | | 04 | ENGINE_LOAD | Calculated Engine Load | | 05 | COOLANT_TEMP | Engine Coolant Temperature | @@ -84,7 +84,7 @@ obd.commands.has_pid(1, 12) # True | 10 | MAF | Air Flow Rate (MAF) | | 11 | THROTTLE_POS | Throttle Position | | 12 | AIR_STATUS | Secondary Air Status | -| 13 | \ | \ | +| 13 | *unsupported* | *unsupported* | | 14 | O2_B1S1 | O2: Bank 1 - Sensor 1 Voltage | | 15 | O2_B1S2 | O2: Bank 1 - Sensor 2 Voltage | | 16 | O2_B1S3 | O2: Bank 1 - Sensor 3 Voltage | @@ -94,8 +94,8 @@ obd.commands.has_pid(1, 12) # True | 1A | O2_B2S3 | O2: Bank 2 - Sensor 3 Voltage | | 1B | O2_B2S4 | O2: Bank 2 - Sensor 4 Voltage | | 1C | OBD_COMPLIANCE | OBD Standards Compliance | -| 1D | \ | \ | -| 1E | \ | \ | +| 1D | *unsupported* | *unsupported* | +| 1E | *unsupported* | *unsupported* | | 1F | RUN_TIME | Engine Run Time | | 20 | PIDS_B | Supported PIDs [21-40] | | 21 | DISTANCE_W_MIL | Distance Traveled with MIL on | @@ -130,10 +130,10 @@ obd.commands.has_pid(1, 12) # True | 3E | CATALYST_TEMP_B1S2 | Catalyst Temperature: Bank 1 - Sensor 2 | | 3F | CATALYST_TEMP_B2S2 | Catalyst Temperature: Bank 2 - Sensor 2 | | 40 | PIDS_C | Supported PIDs [41-60] | -| 41 | \ | \ | -| 42 | \ | \ | -| 43 | \ | \ | -| 44 | \ | \ | +| 41 | *unsupported* | *unsupported* | +| 42 | *unsupported* | *unsupported* | +| 43 | *unsupported* | *unsupported* | +| 44 | *unsupported* | *unsupported* | | 45 | RELATIVE_THROTTLE_POS | Relative throttle position | | 46 | AMBIANT_AIR_TEMP | Ambient air temperature | | 47 | THROTTLE_POS_B | Absolute throttle position B | @@ -144,7 +144,7 @@ obd.commands.has_pid(1, 12) # True | 4C | THROTTLE_ACTUATOR | Commanded throttle actuator | | 4D | RUN_TIME_MIL | Time run with MIL on | | 4E | TIME_SINCE_DTC_CLEARED | Time since trouble codes cleared | -| 4F | \ | \ | +| 4F | *unsupported* | *unsupported* | | 50 | MAX_MAF | Maximum value for mass air flow sensor | | 51 | FUEL_TYPE | Fuel Type | | 52 | ETHANOL_PERCENT | Ethanol Fuel Percent | @@ -160,7 +160,7 @@ obd.commands.has_pid(1, 12) # True | 5C | OIL_TEMP | Engine oil temperature | | 5D | FUEL_INJECT_TIMING | Fuel injection timing | | 5E | FUEL_RATE | Engine fuel rate | -| 5F | \ | \ | +| 5F | *unsupported* | *unsupported* |
From 6322e6c43a0e499a4c855ba520cd009ca5b95381 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 21 Jun 2015 16:14:16 -0400 Subject: [PATCH 228/569] renamed some doc files --- .gitignore | 1 + docs/{OBD Commands.md => Commands.md} | 2 +- docs/{OBD Connections.md => Connections.md} | 0 docs/{index.md => Getting Started.md} | 0 docs/{OBD Responses.md => Responses.md} | 0 mkdocs.yml | 8 ++++---- 6 files changed, 6 insertions(+), 5 deletions(-) rename docs/{OBD Commands.md => Commands.md} (65%) rename docs/{OBD Connections.md => Connections.md} (100%) rename docs/{index.md => Getting Started.md} (100%) rename docs/{OBD Responses.md => Responses.md} (100%) diff --git a/.gitignore b/.gitignore index db4561ea..f2abd626 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ __pycache__/ # Distribution / packaging .Python env/ +env2/ build/ develop-eggs/ dist/ diff --git a/docs/OBD Commands.md b/docs/Commands.md similarity index 65% rename from docs/OBD Commands.md rename to docs/Commands.md index 6de4059c..b7c2527a 100644 --- a/docs/OBD Commands.md +++ b/docs/Commands.md @@ -1,5 +1,5 @@ -An `OBDCommand` is an object used to query information from the vehicle. They contain all of the information neccessary to perform the query, and decode the cars response. +An `OBDCommand`s are objects used to query information from the vehicle. They contain all of the information neccessary to perform the query, and decode the cars response. --- diff --git a/docs/OBD Connections.md b/docs/Connections.md similarity index 100% rename from docs/OBD Connections.md rename to docs/Connections.md diff --git a/docs/index.md b/docs/Getting Started.md similarity index 100% rename from docs/index.md rename to docs/Getting Started.md diff --git a/docs/OBD Responses.md b/docs/Responses.md similarity index 100% rename from docs/OBD Responses.md rename to docs/Responses.md diff --git a/mkdocs.yml b/mkdocs.yml index 43d90b86..1995aa5d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,12 +2,12 @@ site_name: python-OBD repo_url: https://github.com/brendanwhitfield/python-OBD repo_name: GitHub pages: -- Home: 'index.md' -- OBD Connections: 'OBD Connections.md' -- Commands: 'OBD Commands.md' +- Getting Started: 'Getting Started.md' +- OBD Connections: 'Connections.md' +- Commands: 'Commands.md' - Command Tables: 'Command Tables.md' - Custom Commands: 'Custom Commands.md' -- Responses: 'OBD Responses.md' +- Responses: 'Responses.md' - Async Connections: 'Async Connections.md' - Debug: 'Debug.md' - Troubleshooting: 'Troubleshooting.md' From a42be2370e301d8a05d9b020c66923a6e276de22 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 21 Jun 2015 17:22:29 -0400 Subject: [PATCH 229/569] combined Commands and Command Tables docs, minor tweaks --- docs/Command Tables.md | 244 ---------------------------------------- docs/Commands.md | 220 +++++++++++++++++++++++++++++++++++- docs/Getting Started.md | 40 +++++-- docs/Responses.md | 21 +++- mkdocs.yml | 3 +- 5 files changed, 271 insertions(+), 257 deletions(-) delete mode 100644 docs/Command Tables.md diff --git a/docs/Command Tables.md b/docs/Command Tables.md deleted file mode 100644 index 3c258da6..00000000 --- a/docs/Command Tables.md +++ /dev/null @@ -1,244 +0,0 @@ - -# Lookup - -Python-OBD has built in tables for the most common commands. They can be looked up by name, or by mode & PID. - -```python -import obd - -c = obd.commands.RPM - -# OR - -c = obd.commands['RPM'] - -# OR - -c = obd.commands[1][12] # mode 1, PID 12 (RPM) -``` - -The `commands` table also has a few helper methods for determining if a particular name or PID is present. - ---- - -### has_command(command) - -Checks the internal command tables for the existance of the given `OBDCommand` object. Commands are compared by mode and PID value. - -```python -import obd -obd.commands.has_command(obd.commands.RPM) # True -``` - ---- - -### has_name(name) - -Checks the internal command tables for a command with the given name. This is also the function of the `in` operator. - -```python -import obd - -obd.commands.has_name('RPM') # True - -# OR - -'RPM' in obd.commands # True -``` - ---- - -### has_pid(mode, pid) - -Checks the internal command tables for a command with the given mode and PID. - -```python -import obd -obd.commands.has_pid(1, 12) # True -``` - ---- - -
- -# Mode 01 - -|PID | Name | Description | -|----|---------------------------|-----------------------------------------| -| 00 | PIDS_A | Supported PIDs [01-20] | -| 01 | STATUS | Status since DTCs cleared | -| 02 | *unsupported* | *unsupported* | -| 03 | FUEL_STATUS | Fuel System Status | -| 04 | ENGINE_LOAD | Calculated Engine Load | -| 05 | COOLANT_TEMP | Engine Coolant Temperature | -| 06 | SHORT_FUEL_TRIM_1 | Short Term Fuel Trim - Bank 1 | -| 07 | LONG_FUEL_TRIM_1 | Long Term Fuel Trim - Bank 1 | -| 08 | SHORT_FUEL_TRIM_2 | Short Term Fuel Trim - Bank 2 | -| 09 | LONG_FUEL_TRIM_2 | Long Term Fuel Trim - Bank 2 | -| 0A | FUEL_PRESSURE | Fuel Pressure | -| 0B | INTAKE_PRESSURE | Intake Manifold Pressure | -| 0C | RPM | Engine RPM | -| 0D | SPEED | Vehicle Speed | -| 0E | TIMING_ADVANCE | Timing Advance | -| 0F | INTAKE_TEMP | Intake Air Temp | -| 10 | MAF | Air Flow Rate (MAF) | -| 11 | THROTTLE_POS | Throttle Position | -| 12 | AIR_STATUS | Secondary Air Status | -| 13 | *unsupported* | *unsupported* | -| 14 | O2_B1S1 | O2: Bank 1 - Sensor 1 Voltage | -| 15 | O2_B1S2 | O2: Bank 1 - Sensor 2 Voltage | -| 16 | O2_B1S3 | O2: Bank 1 - Sensor 3 Voltage | -| 17 | O2_B1S4 | O2: Bank 1 - Sensor 4 Voltage | -| 18 | O2_B2S1 | O2: Bank 2 - Sensor 1 Voltage | -| 19 | O2_B2S2 | O2: Bank 2 - Sensor 2 Voltage | -| 1A | O2_B2S3 | O2: Bank 2 - Sensor 3 Voltage | -| 1B | O2_B2S4 | O2: Bank 2 - Sensor 4 Voltage | -| 1C | OBD_COMPLIANCE | OBD Standards Compliance | -| 1D | *unsupported* | *unsupported* | -| 1E | *unsupported* | *unsupported* | -| 1F | RUN_TIME | Engine Run Time | -| 20 | PIDS_B | Supported PIDs [21-40] | -| 21 | DISTANCE_W_MIL | Distance Traveled with MIL on | -| 22 | FUEL_RAIL_PRESSURE_VAC | Fuel Rail Pressure (relative to vacuum) | -| 23 | FUEL_RAIL_PRESSURE_DIRECT | Fuel Rail Pressure (direct inject) | -| 24 | O2_S1_WR_VOLTAGE | 02 Sensor 1 WR Lambda Voltage | -| 25 | O2_S2_WR_VOLTAGE | 02 Sensor 2 WR Lambda Voltage | -| 26 | O2_S3_WR_VOLTAGE | 02 Sensor 3 WR Lambda Voltage | -| 27 | O2_S4_WR_VOLTAGE | 02 Sensor 4 WR Lambda Voltage | -| 28 | O2_S5_WR_VOLTAGE | 02 Sensor 5 WR Lambda Voltage | -| 29 | O2_S6_WR_VOLTAGE | 02 Sensor 6 WR Lambda Voltage | -| 2A | O2_S7_WR_VOLTAGE | 02 Sensor 7 WR Lambda Voltage | -| 2B | O2_S8_WR_VOLTAGE | 02 Sensor 8 WR Lambda Voltage | -| 2C | COMMANDED_EGR | Commanded EGR | -| 2D | EGR_ERROR | EGR Error | -| 2E | EVAPORATIVE_PURGE | Commanded Evaporative Purge | -| 2F | FUEL_LEVEL | Fuel Level Input | -| 30 | WARMUPS_SINCE_DTC_CLEAR | Number of warm-ups since codes cleared | -| 31 | DISTANCE_SINCE_DTC_CLEAR | Distance traveled since codes cleared | -| 32 | EVAP_VAPOR_PRESSURE | Evaporative system vapor pressure | -| 33 | BAROMETRIC_PRESSURE | Barometric Pressure | -| 34 | O2_S1_WR_CURRENT | 02 Sensor 1 WR Lambda Current | -| 35 | O2_S2_WR_CURRENT | 02 Sensor 2 WR Lambda Current | -| 36 | O2_S3_WR_CURRENT | 02 Sensor 3 WR Lambda Current | -| 37 | O2_S4_WR_CURRENT | 02 Sensor 4 WR Lambda Current | -| 38 | O2_S5_WR_CURRENT | 02 Sensor 5 WR Lambda Current | -| 39 | O2_S6_WR_CURRENT | 02 Sensor 6 WR Lambda Current | -| 3A | O2_S7_WR_CURRENT | 02 Sensor 7 WR Lambda Current | -| 3B | O2_S8_WR_CURRENT | 02 Sensor 8 WR Lambda Current | -| 3C | CATALYST_TEMP_B1S1 | Catalyst Temperature: Bank 1 - Sensor 1 | -| 3D | CATALYST_TEMP_B2S1 | Catalyst Temperature: Bank 2 - Sensor 1 | -| 3E | CATALYST_TEMP_B1S2 | Catalyst Temperature: Bank 1 - Sensor 2 | -| 3F | CATALYST_TEMP_B2S2 | Catalyst Temperature: Bank 2 - Sensor 2 | -| 40 | PIDS_C | Supported PIDs [41-60] | -| 41 | *unsupported* | *unsupported* | -| 42 | *unsupported* | *unsupported* | -| 43 | *unsupported* | *unsupported* | -| 44 | *unsupported* | *unsupported* | -| 45 | RELATIVE_THROTTLE_POS | Relative throttle position | -| 46 | AMBIANT_AIR_TEMP | Ambient air temperature | -| 47 | THROTTLE_POS_B | Absolute throttle position B | -| 48 | THROTTLE_POS_C | Absolute throttle position C | -| 49 | ACCELERATOR_POS_D | Accelerator pedal position D | -| 4A | ACCELERATOR_POS_E | Accelerator pedal position E | -| 4B | ACCELERATOR_POS_F | Accelerator pedal position F | -| 4C | THROTTLE_ACTUATOR | Commanded throttle actuator | -| 4D | RUN_TIME_MIL | Time run with MIL on | -| 4E | TIME_SINCE_DTC_CLEARED | Time since trouble codes cleared | -| 4F | *unsupported* | *unsupported* | -| 50 | MAX_MAF | Maximum value for mass air flow sensor | -| 51 | FUEL_TYPE | Fuel Type | -| 52 | ETHANOL_PERCENT | Ethanol Fuel Percent | -| 53 | EVAP_VAPOR_PRESSURE_ABS | Absolute Evap system Vapor Pressure | -| 54 | EVAP_VAPOR_PRESSURE_ALT | Evap system vapor pressure | -| 55 | SHORT_O2_TRIM_B1 | Short term secondary O2 trim - Bank 1 | -| 56 | LONG_O2_TRIM_B1 | Long term secondary O2 trim - Bank 1 | -| 57 | SHORT_O2_TRIM_B2 | Short term secondary O2 trim - Bank 2 | -| 58 | LONG_O2_TRIM_B2 | Long term secondary O2 trim - Bank 2 | -| 59 | FUEL_RAIL_PRESSURE_ABS | Fuel rail pressure (absolute) | -| 5A | RELATIVE_ACCEL_POS | Relative accelerator pedal position | -| 5B | HYBRID_BATTERY_REMAINING | Hybrid battery pack remaining life | -| 5C | OIL_TEMP | Engine oil temperature | -| 5D | FUEL_INJECT_TIMING | Fuel injection timing | -| 5E | FUEL_RATE | Engine fuel rate | -| 5F | *unsupported* | *unsupported* | - -
- ---- - -
- -# Mode 02 - -Mode 02 commands are the same as mode 01, but are metrics from when the last DTC occurred (the freeze frame). To access them by name, simple prepend `DTC_` to the Mode 01 command name. - -```python -import obd - -obd.commands.RPM # the Mode 01 command -# vs. -obd.commands.DTC_RPM # the Mode 02 command -``` - -
- ---- - -
- -# Mode 03 - -Mode 03 contains a single command `GET_DTC` which requests all diagnostic trouble codes from the vehicle's engine. - -|PID | Name | Description | -|-----|---------|-----------------------------------------| -| N/A | GET_DTC | Get Diagnostic Trouble Codes | - -This command requests all diagnostic trouble codes from the vehicle's engine. The `value` field of the response object will contain a list of tuples, where each tuple contains the DTC, and a string description of that DTC (if available). - -```python -import obd -connection = obd.OBD() -r = connection.query(obd.commands.GET_DTC) -print(r.value) - -''' -example output: -[ - ("P0030", "HO2S Heater Control Circuit"), - ("P1367", "Unknown error code") -] -''' -``` - -
- ---- - -
- -# Mode 04 - -|PID | Name | Description | -|-----|-----------|-----------------------------------------| -| N/A | CLEAR_DTC | Clear DTCs and Freeze data | - -
- ---- - -
- -# Mode 07 - -The return value will be encoded in the same structure as the Mode 03 `GET_DTC` command. - -|PID | Name | Description | -|-----|----------------|------------------------------| -| N/A | GET_FREEZE_DTC | Get Freeze DTCs | - -
- ---- - -
diff --git a/docs/Commands.md b/docs/Commands.md index b7c2527a..4d966560 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -1,6 +1,224 @@ -An `OBDCommand`s are objects used to query information from the vehicle. They contain all of the information neccessary to perform the query, and decode the cars response. +# Lookup + +`OBDCommand`s are objects used to query information from the vehicle. They contain all of the information neccessary to perform the query, and decode the cars response. Python-OBD has built in tables for the most common commands. They can be looked up by name, or by mode & PID. + +```python +import obd + +c = obd.commands.RPM + +# OR + +c = obd.commands['RPM'] + +# OR + +c = obd.commands[1][12] # mode 1, PID 12 (RPM) +``` + +The `commands` table also has a few helper methods for determining if a particular name or PID is present. --- +### has_command(command) + +Checks the internal command tables for the existance of the given `OBDCommand` object. Commands are compared by mode and PID value. + +```python +import obd +obd.commands.has_command(obd.commands.RPM) # True +``` + +--- + +### has_name(name) + +Checks the internal command tables for a command with the given name. This is also the function of the `in` operator. + +```python +import obd + +obd.commands.has_name('RPM') # True + +# OR + +'RPM' in obd.commands # True +``` + +--- + +### has_pid(mode, pid) + +Checks the internal command tables for a command with the given mode and PID. + +```python +import obd +obd.commands.has_pid(1, 12) # True +``` + +--- + +
+ +# Mode 01 + +|PID | Name | Description | +|----|---------------------------|-----------------------------------------| +| 00 | PIDS_A | Supported PIDs [01-20] | +| 01 | STATUS | Status since DTCs cleared | +| 02 | *unsupported* | *unsupported* | +| 03 | FUEL_STATUS | Fuel System Status | +| 04 | ENGINE_LOAD | Calculated Engine Load | +| 05 | COOLANT_TEMP | Engine Coolant Temperature | +| 06 | SHORT_FUEL_TRIM_1 | Short Term Fuel Trim - Bank 1 | +| 07 | LONG_FUEL_TRIM_1 | Long Term Fuel Trim - Bank 1 | +| 08 | SHORT_FUEL_TRIM_2 | Short Term Fuel Trim - Bank 2 | +| 09 | LONG_FUEL_TRIM_2 | Long Term Fuel Trim - Bank 2 | +| 0A | FUEL_PRESSURE | Fuel Pressure | +| 0B | INTAKE_PRESSURE | Intake Manifold Pressure | +| 0C | RPM | Engine RPM | +| 0D | SPEED | Vehicle Speed | +| 0E | TIMING_ADVANCE | Timing Advance | +| 0F | INTAKE_TEMP | Intake Air Temp | +| 10 | MAF | Air Flow Rate (MAF) | +| 11 | THROTTLE_POS | Throttle Position | +| 12 | AIR_STATUS | Secondary Air Status | +| 13 | *unsupported* | *unsupported* | +| 14 | O2_B1S1 | O2: Bank 1 - Sensor 1 Voltage | +| 15 | O2_B1S2 | O2: Bank 1 - Sensor 2 Voltage | +| 16 | O2_B1S3 | O2: Bank 1 - Sensor 3 Voltage | +| 17 | O2_B1S4 | O2: Bank 1 - Sensor 4 Voltage | +| 18 | O2_B2S1 | O2: Bank 2 - Sensor 1 Voltage | +| 19 | O2_B2S2 | O2: Bank 2 - Sensor 2 Voltage | +| 1A | O2_B2S3 | O2: Bank 2 - Sensor 3 Voltage | +| 1B | O2_B2S4 | O2: Bank 2 - Sensor 4 Voltage | +| 1C | OBD_COMPLIANCE | OBD Standards Compliance | +| 1D | *unsupported* | *unsupported* | +| 1E | *unsupported* | *unsupported* | +| 1F | RUN_TIME | Engine Run Time | +| 20 | PIDS_B | Supported PIDs [21-40] | +| 21 | DISTANCE_W_MIL | Distance Traveled with MIL on | +| 22 | FUEL_RAIL_PRESSURE_VAC | Fuel Rail Pressure (relative to vacuum) | +| 23 | FUEL_RAIL_PRESSURE_DIRECT | Fuel Rail Pressure (direct inject) | +| 24 | O2_S1_WR_VOLTAGE | 02 Sensor 1 WR Lambda Voltage | +| 25 | O2_S2_WR_VOLTAGE | 02 Sensor 2 WR Lambda Voltage | +| 26 | O2_S3_WR_VOLTAGE | 02 Sensor 3 WR Lambda Voltage | +| 27 | O2_S4_WR_VOLTAGE | 02 Sensor 4 WR Lambda Voltage | +| 28 | O2_S5_WR_VOLTAGE | 02 Sensor 5 WR Lambda Voltage | +| 29 | O2_S6_WR_VOLTAGE | 02 Sensor 6 WR Lambda Voltage | +| 2A | O2_S7_WR_VOLTAGE | 02 Sensor 7 WR Lambda Voltage | +| 2B | O2_S8_WR_VOLTAGE | 02 Sensor 8 WR Lambda Voltage | +| 2C | COMMANDED_EGR | Commanded EGR | +| 2D | EGR_ERROR | EGR Error | +| 2E | EVAPORATIVE_PURGE | Commanded Evaporative Purge | +| 2F | FUEL_LEVEL | Fuel Level Input | +| 30 | WARMUPS_SINCE_DTC_CLEAR | Number of warm-ups since codes cleared | +| 31 | DISTANCE_SINCE_DTC_CLEAR | Distance traveled since codes cleared | +| 32 | EVAP_VAPOR_PRESSURE | Evaporative system vapor pressure | +| 33 | BAROMETRIC_PRESSURE | Barometric Pressure | +| 34 | O2_S1_WR_CURRENT | 02 Sensor 1 WR Lambda Current | +| 35 | O2_S2_WR_CURRENT | 02 Sensor 2 WR Lambda Current | +| 36 | O2_S3_WR_CURRENT | 02 Sensor 3 WR Lambda Current | +| 37 | O2_S4_WR_CURRENT | 02 Sensor 4 WR Lambda Current | +| 38 | O2_S5_WR_CURRENT | 02 Sensor 5 WR Lambda Current | +| 39 | O2_S6_WR_CURRENT | 02 Sensor 6 WR Lambda Current | +| 3A | O2_S7_WR_CURRENT | 02 Sensor 7 WR Lambda Current | +| 3B | O2_S8_WR_CURRENT | 02 Sensor 8 WR Lambda Current | +| 3C | CATALYST_TEMP_B1S1 | Catalyst Temperature: Bank 1 - Sensor 1 | +| 3D | CATALYST_TEMP_B2S1 | Catalyst Temperature: Bank 2 - Sensor 1 | +| 3E | CATALYST_TEMP_B1S2 | Catalyst Temperature: Bank 1 - Sensor 2 | +| 3F | CATALYST_TEMP_B2S2 | Catalyst Temperature: Bank 2 - Sensor 2 | +| 40 | PIDS_C | Supported PIDs [41-60] | +| 41 | *unsupported* | *unsupported* | +| 42 | *unsupported* | *unsupported* | +| 43 | *unsupported* | *unsupported* | +| 44 | *unsupported* | *unsupported* | +| 45 | RELATIVE_THROTTLE_POS | Relative throttle position | +| 46 | AMBIANT_AIR_TEMP | Ambient air temperature | +| 47 | THROTTLE_POS_B | Absolute throttle position B | +| 48 | THROTTLE_POS_C | Absolute throttle position C | +| 49 | ACCELERATOR_POS_D | Accelerator pedal position D | +| 4A | ACCELERATOR_POS_E | Accelerator pedal position E | +| 4B | ACCELERATOR_POS_F | Accelerator pedal position F | +| 4C | THROTTLE_ACTUATOR | Commanded throttle actuator | +| 4D | RUN_TIME_MIL | Time run with MIL on | +| 4E | TIME_SINCE_DTC_CLEARED | Time since trouble codes cleared | +| 4F | *unsupported* | *unsupported* | +| 50 | MAX_MAF | Maximum value for mass air flow sensor | +| 51 | FUEL_TYPE | Fuel Type | +| 52 | ETHANOL_PERCENT | Ethanol Fuel Percent | +| 53 | EVAP_VAPOR_PRESSURE_ABS | Absolute Evap system Vapor Pressure | +| 54 | EVAP_VAPOR_PRESSURE_ALT | Evap system vapor pressure | +| 55 | SHORT_O2_TRIM_B1 | Short term secondary O2 trim - Bank 1 | +| 56 | LONG_O2_TRIM_B1 | Long term secondary O2 trim - Bank 1 | +| 57 | SHORT_O2_TRIM_B2 | Short term secondary O2 trim - Bank 2 | +| 58 | LONG_O2_TRIM_B2 | Long term secondary O2 trim - Bank 2 | +| 59 | FUEL_RAIL_PRESSURE_ABS | Fuel rail pressure (absolute) | +| 5A | RELATIVE_ACCEL_POS | Relative accelerator pedal position | +| 5B | HYBRID_BATTERY_REMAINING | Hybrid battery pack remaining life | +| 5C | OIL_TEMP | Engine oil temperature | +| 5D | FUEL_INJECT_TIMING | Fuel injection timing | +| 5E | FUEL_RATE | Engine fuel rate | +| 5F | *unsupported* | *unsupported* | + +
+ +# Mode 02 + +Mode 02 commands are the same as mode 01, but are metrics from when the last DTC occurred (the freeze frame). To access them by name, simple prepend `DTC_` to the Mode 01 command name. + +```python +import obd + +obd.commands.RPM # the Mode 01 command +# vs. +obd.commands.DTC_RPM # the Mode 02 command +``` + +
+ +# Mode 03 + +Mode 03 contains a single command `GET_DTC` which requests all diagnostic trouble codes from the vehicle's engine. + +|PID | Name | Description | +|-----|---------|-----------------------------------------| +| N/A | GET_DTC | Get Diagnostic Trouble Codes | + +This command requests all diagnostic trouble codes from the vehicle's engine. The `value` field of the response object will contain a list of tuples, where each tuple contains the DTC, and a string description of that DTC (if available). + +```python +import obd +connection = obd.OBD() +r = connection.query(obd.commands.GET_DTC) +print(r.value) + +''' +example output: +[ + ("P0030", "HO2S Heater Control Circuit"), + ("P1367", "Unknown error code") +] +''' +``` + +
+ +# Mode 04 + +|PID | Name | Description | +|-----|-----------|-----------------------------------------| +| N/A | CLEAR_DTC | Clear DTCs and Freeze data | + +
+ +# Mode 07 + +The return value will be encoded in the same structure as the Mode 03 `GET_DTC` command. + +|PID | Name | Description | +|-----|----------------|------------------------------| +| N/A | GET_FREEZE_DTC | Get Freeze DTCs | +
diff --git a/docs/Getting Started.md b/docs/Getting Started.md index 0921fe75..3cf6585c 100644 --- a/docs/Getting Started.md +++ b/docs/Getting Started.md @@ -1,19 +1,45 @@ +# Welcome + +Python-OBD is a library for handling data from a car's [**O**n-**B**oard **D**iagnostics port](https://en.wikipedia.org/wiki/On-board_diagnostics) (OBD-II). It can stream real time sensor data, perform diagnostics (such as reading check-engine codes), and is fit for the Raspberry Pi. This library is designed to work with standard [ELM327 OBD-II adapters](http://www.amazon.com/s/ref=nb_sb_noss?field-keywords=elm327). + +
+ # Installation -Run the following command to download/install the latest release from pypi: +Install the latest release from pypi: - $ pip install obd +```shell +$ pip install obd +``` If you are using a bluetooth adapter, you will need to install the following packages: - $ sudo apt-get install bluetooth bluez-utils blueman +```shell +$ sudo apt-get install bluetooth bluez-utils blueman +``` ---- +
+ +# Basic Usage + +```python +import obd + +connection = obd.OBD() # auto-connects to USB or RF port + +cmd = obd.commands.RPM # select an OBD command (sensor) + +response = connection.query(cmd) # send the command, and parse the response + +print(response.value) +print(response.unit) +``` + +
-# Dependencies +# License -+ pySerial -+ OBD-II adapter (ELM327 Bluetooth Adapter or ELM327 USB Cable) +GNU General Public License V2 --- diff --git a/docs/Responses.md b/docs/Responses.md index 6d35b884..fe72b3b6 100644 --- a/docs/Responses.md +++ b/docs/Responses.md @@ -8,11 +8,26 @@ The `query()` function returns `OBDResponse` objects. These objects have the fol | message | The internal `Message` object containing the raw response from the car | | time | Timestamp of response (as given by [`time.time()`](https://docs.python.org/2/library/time.html#time.time)) | -The `value` property typically contains numeric values, but can also hold complex structures (depending upon the command being decoded). +The `value` property typically contains numeric values, but can also hold complex structures (depending upon the command that was sent). -If python-OBD is unable to retrieve a response from the car, an empty `OBDResponse` object will be returned. Use `is_null()` to check for empty responses. -## Units +--- + +### is_null() + +Use this function to check if a response is empty. Python-OBD will emit empty responses when it is unable to retrieve data from the car. + +```python +r = connection.query(obd.commands.RPM) + +if not r.is_null(): + print(r.value) +``` + +--- + + +# Units Unit values can be found in the `Unit` class (enum). diff --git a/mkdocs.yml b/mkdocs.yml index 1995aa5d..e3d53d65 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,10 +5,9 @@ pages: - Getting Started: 'Getting Started.md' - OBD Connections: 'Connections.md' - Commands: 'Commands.md' -- Command Tables: 'Command Tables.md' -- Custom Commands: 'Custom Commands.md' - Responses: 'Responses.md' - Async Connections: 'Async Connections.md' +- Custom Commands: 'Custom Commands.md' - Debug: 'Debug.md' - Troubleshooting: 'Troubleshooting.md' From 440aac7f583a0c01f979b870e16195c57a3fed99 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 5 Jul 2015 14:49:43 -0400 Subject: [PATCH 230/569] removed some trailing whitespace --- obd/async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/async.py b/obd/async.py index 91f730bc..57e1ae0d 100644 --- a/obd/async.py +++ b/obd/async.py @@ -52,7 +52,7 @@ def __init__(self, portstr=None, baudrate=38400): @property def running(self): - return self.__running + return self.__running def start(self): From aaf5e6a52f288e814bddf3ab8d54d393fa998262 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 5 Jul 2015 15:17:04 -0400 Subject: [PATCH 231/569] bump to version 0.4.1 --- obd/__version__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/__version__.py b/obd/__version__.py index 652a8f47..489c7e19 100644 --- a/obd/__version__.py +++ b/obd/__version__.py @@ -1,2 +1,2 @@ -__version__ = '0.4.0' +__version__ = '0.4.1' diff --git a/setup.py b/setup.py index a43a4f5c..02f47254 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="obd", - version="0.4.0", + version="0.4.1", description=("Serial module for handling live sensor data from a vehicle's OBD-II port"), classifiers=[ "Operating System :: POSIX :: Linux", From eebf89fb2f977789ef482a9b822fa2293caf981d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 15 Oct 2015 19:15:30 -0400 Subject: [PATCH 232/569] mkdocs and readthedocs needs the home page to be index.md --- docs/{Getting Started.md => index.md} | 2 +- mkdocs.yml | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) rename docs/{Getting Started.md => index.md} (89%) diff --git a/docs/Getting Started.md b/docs/index.md similarity index 89% rename from docs/Getting Started.md rename to docs/index.md index 3cf6585c..ef0afeaf 100644 --- a/docs/Getting Started.md +++ b/docs/index.md @@ -12,7 +12,7 @@ Install the latest release from pypi: $ pip install obd ``` -If you are using a bluetooth adapter, you will need to install the following packages: +If you are using a bluetooth adapter on Debian-based linux, you will need to install the following packages: ```shell $ sudo apt-get install bluetooth bluez-utils blueman diff --git a/mkdocs.yml b/mkdocs.yml index e3d53d65..2a6dc499 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,13 +2,13 @@ site_name: python-OBD repo_url: https://github.com/brendanwhitfield/python-OBD repo_name: GitHub pages: -- Getting Started: 'Getting Started.md' -- OBD Connections: 'Connections.md' -- Commands: 'Commands.md' -- Responses: 'Responses.md' -- Async Connections: 'Async Connections.md' -- Custom Commands: 'Custom Commands.md' -- Debug: 'Debug.md' -- Troubleshooting: 'Troubleshooting.md' +- 'Getting Started': 'index.md' +- 'OBD Connections': 'Connections.md' +- 'Commands': 'Commands.md' +- 'Responses': 'Responses.md' +- 'Async Connections': 'Async Connections.md' +- 'Custom Commands': 'Custom Commands.md' +- 'Debug': 'Debug.md' +- 'Troubleshooting': 'Troubleshooting.md' theme: readthedocs From f3d2db8e21d45bd0e796dee458438f0740574b67 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 16 Oct 2015 13:15:14 -0400 Subject: [PATCH 233/569] switched doc link to readthedocs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 76853cb4..e8459fd6 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ print(response.unit) Documentation ------------- -[Visit the GitHub Wiki!](http://github.com/brendanwhitfield/python-OBD/wiki) +Available at [python-obd.readthedocs.org](http://python-obd.readthedocs.org/en/latest/) Commands -------- From 542d6bd2d0d86a044d933d43138740699e5d25ba Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 1 Jun 2015 16:45:22 -0400 Subject: [PATCH 234/569] moved response related objects to OBDResponse.py --- obd/OBDCommand.py | 2 +- obd/OBDResponse.py | 108 +++++++++++++++++++++++++++++++++++++++++++++ obd/async.py | 4 +- obd/codes.py | 12 ++--- obd/decoders.py | 1 + obd/obd.py | 8 ++-- obd/utils.py | 61 ------------------------- tests/test_OBD.py | 2 +- 8 files changed, 123 insertions(+), 75 deletions(-) create mode 100644 obd/OBDResponse.py diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 4c20cbc0..186135e6 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -65,7 +65,7 @@ def __call__(self, message): # create the response object with the raw data recieved # and reference to original command - r = Response(self, message) + r = OBDResponse(self, message) # combine the bytes back into a hex string # TODO: rewrite decoders to handle raw byte arrays diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py new file mode 100644 index 00000000..2e2fb13f --- /dev/null +++ b/obd/OBDResponse.py @@ -0,0 +1,108 @@ + +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2009 Peter J. Creath # +# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# # +######################################################################## +# # +# OBDOBDResponse.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## + + + +import time + + + +class Unit: + """ All unit constants used in python-OBD """ + + NONE = None + RATIO = "Ratio" + COUNT = "Count" + PERCENT = "%" + RPM = "RPM" + VOLT = "Volt" + F = "F" + C = "C" + SEC = "Second" + MIN = "Minute" + PA = "Pa" + KPA = "kPa" + PSI = "psi" + KPH = "kph" + MPH = "mph" + DEGREES = "Degrees" + GPS = "Grams per Second" + MA = "mA" + KM = "km" + LPH = "Liters per Hour" + + + +class OBDResponse(): + """ Standard response object for any OBDCommand """ + + def __init__(self, command=None, message=None): + self.command = command + self.message = message + self.value = None + self.unit = Unit.NONE + self.time = time.time() + + def is_null(self): + return (self.message == None) or (self.value == None) + + def __str__(self): + if self.unit != Unit.NONE: + return "%s %s" % (str(self.value), str(self.unit)) + else: + return str(self.value) + + + +""" + Special value types used in OBDResponses + instantiated in decoders.py +""" + + +class Status(): + def __init__(self): + self.MIL = False + self.DTC_count = 0 + self.ignition_type = "" + self.tests = [] + + +class Test(): + def __init__(self, name, available, incomplete): + self.name = name + self.available = available + self.incomplete = incomplete + + def __str__(self): + a = "Available" if self.available else "Unavailable" + c = "Incomplete" if self.incomplete else "Complete" + return "Test %s: %s, %s" % (self.name, a, c) diff --git a/obd/async.py b/obd/async.py index 57e1ae0d..77a00043 100644 --- a/obd/async.py +++ b/obd/async.py @@ -31,7 +31,7 @@ import time import threading -from .utils import Response +from .utils import OBDResponse from .debug import debug from . import OBD @@ -197,7 +197,7 @@ def query(self, c): if c in self.__commands: return self.__commands[c] else: - return Response() + return OBDResponse() def run(self): diff --git a/obd/codes.py b/obd/codes.py index 3079b87b..2b6d277e 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -162,19 +162,19 @@ "P0130": "O2 Sensor Circuit", "P0131": "O2 Sensor Circuit Low Voltage", "P0132": "O2 Sensor Circuit High Voltage", - "P0133": "O2 Sensor Circuit Slow Response", + "P0133": "O2 Sensor Circuit Slow OBDResponse", "P0134": "O2 Sensor Circuit No Activity Detected", "P0135": "O2 Sensor Heater Circuit", "P0136": "O2 Sensor Circuit", "P0137": "O2 Sensor Circuit Low Voltage", "P0138": "O2 Sensor Circuit High Voltage", - "P0139": "O2 Sensor Circuit Slow Response", + "P0139": "O2 Sensor Circuit Slow OBDResponse", "P0140": "O2 Sensor Circuit No Activity Detected", "P0141": "O2 Sensor Heater Circuit", "P0142": "O2 Sensor Circuit", "P0143": "O2 Sensor Circuit Low Voltage", "P0144": "O2 Sensor Circuit High Voltage", - "P0145": "O2 Sensor Circuit Slow Response", + "P0145": "O2 Sensor Circuit Slow OBDResponse", "P0146": "O2 Sensor Circuit No Activity Detected", "P0147": "O2 Sensor Heater Circuit", "P0148": "Fuel Delivery Error", @@ -182,19 +182,19 @@ "P0150": "O2 Sensor Circuit", "P0151": "O2 Sensor Circuit Low Voltage", "P0152": "O2 Sensor Circuit High Voltage", - "P0153": "O2 Sensor Circuit Slow Response", + "P0153": "O2 Sensor Circuit Slow OBDResponse", "P0154": "O2 Sensor Circuit No Activity Detected", "P0155": "O2 Sensor Heater Circuit", "P0156": "O2 Sensor Circuit", "P0157": "O2 Sensor Circuit Low Voltage", "P0158": "O2 Sensor Circuit High Voltage", - "P0159": "O2 Sensor Circuit Slow Response", + "P0159": "O2 Sensor Circuit Slow OBDResponse", "P0160": "O2 Sensor Circuit No Activity Detected", "P0161": "O2 Sensor Heater Circuit", "P0162": "O2 Sensor Circuit", "P0163": "O2 Sensor Circuit Low Voltage", "P0164": "O2 Sensor Circuit High Voltage", - "P0165": "O2 Sensor Circuit Slow Response", + "P0165": "O2 Sensor Circuit Slow OBDResponse", "P0166": "O2 Sensor Circuit No Activity Detected", "P0167": "O2 Sensor Heater Circuit", "P0168": "Fuel Temperature Too High", diff --git a/obd/decoders.py b/obd/decoders.py index a04a44c1..e4d74408 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -33,6 +33,7 @@ from .utils import * from .codes import * from .debug import debug +from .OBDResponse import Unit, Status, Test ''' All decoders take the form: diff --git a/obd/obd.py b/obd/obd.py index 4d057111..0923ca10 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -34,7 +34,7 @@ from .__version__ import __version__ from .elm327 import ELM327 from .commands import commands -from .utils import scanSerial, Response +from .utils import scanSerial, OBDResponse from .debug import debug @@ -169,7 +169,7 @@ def __send(self, c): if not self.is_connected(): debug("Query failed, no connection available", True) - return Response() # return empty response + return OBDResponse() # return empty response debug("Sending command: %s" % str(c)) @@ -177,7 +177,7 @@ def __send(self, c): m = self.port.send_and_parse(c.get_command()) if m is None: - return Response() # return empty response + return OBDResponse() # return empty response else: return c(m) # compute a response object @@ -193,4 +193,4 @@ def query(self, c, force=False): return self.__send(c) else: debug("'%s' is not supported" % str(c), True) - return Response() # return empty response + return OBDResponse() # return empty response diff --git a/obd/utils.py b/obd/utils.py index c98fa6f7..c4737097 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -32,72 +32,11 @@ import serial import errno import string -import time import glob import sys from .debug import debug -class Unit: - NONE = None - RATIO = "Ratio" - COUNT = "Count" - PERCENT = "%" - RPM = "RPM" - VOLT = "Volt" - F = "F" - C = "C" - SEC = "Second" - MIN = "Minute" - PA = "Pa" - KPA = "kPa" - PSI = "psi" - KPH = "kph" - MPH = "mph" - DEGREES = "Degrees" - GPS = "Grams per Second" - MA = "mA" - KM = "km" - LPH = "Liters per Hour" - - -class Response(): - def __init__(self, command=None, message=None): - self.command = command - self.message = message - self.value = None - self.unit = Unit.NONE - self.time = time.time() - - def is_null(self): - return (self.message == None) or (self.value == None) - - def __str__(self): - if self.unit != Unit.NONE: - return "%s %s" % (str(self.value), str(self.unit)) - else: - return str(self.value) - - -class Status(): - def __init__(self): - self.MIL = False - self.DTC_count = 0 - self.ignition_type = "" - self.tests = [] - - -class Test(): - def __init__(self, name, available, incomplete): - self.name = name - self.available = available - self.incomplete = incomplete - - def __str__(self): - a = "Available" if self.available else "Unavailable" - c = "Incomplete" if self.incomplete else "Complete" - return "Test %s: %s, %s" % (self.name, a, c) - def ascii_to_bytes(a): b = [] diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 90d70c11..198b069c 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -1,6 +1,6 @@ import obd -from obd.utils import Response +from obd.utils import OBDResponse from obd.commands import OBDCommand from obd.decoders import noop from obd.protocols import SAE_J1850_PWM From 68790301b14eb63be8813ae2b5c2368aed4538b3 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 1 Jun 2015 16:51:11 -0400 Subject: [PATCH 235/569] typo in GPL header --- obd/OBDResponse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py index 2e2fb13f..4c6f8459 100644 --- a/obd/OBDResponse.py +++ b/obd/OBDResponse.py @@ -10,7 +10,7 @@ # # ######################################################################## # # -# OBDOBDResponse.py # +# OBDResponse.py # # # # This file is part of python-OBD (a derivative of pyOBD) # # # From 5afb8c500fb6e5592661bf454a8c823c2fef2946 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 1 Jun 2015 18:37:29 -0400 Subject: [PATCH 236/569] fixed lingering OBDResponse imports, added status flag and UnknownProtocol --- obd/OBDCommand.py | 1 + obd/__init__.py | 3 +- obd/async.py | 2 +- obd/elm327.py | 24 ++++++++------- obd/obd.py | 17 ++++++++--- obd/protocols/README.md | 1 + obd/protocols/__init__.py | 2 ++ obd/protocols/protocol_unknown.py | 51 +++++++++++++++++++++++++++++++ obd/utils.py | 8 +++++ tests/test_OBD.py | 4 +-- tests/test_decoders.py | 2 +- 11 files changed, 95 insertions(+), 20 deletions(-) create mode 100644 obd/protocols/protocol_unknown.py diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 186135e6..b88c857f 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -32,6 +32,7 @@ import re from .utils import * from .debug import debug +from .OBDResponse import OBDResponse class OBDCommand(): diff --git a/obd/__init__.py b/obd/__init__.py index 74153872..6e43fd23 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -39,7 +39,8 @@ from .__version__ import __version__ from .obd import OBD from .OBDCommand import OBDCommand +from .OBDResponse import Unit from .commands import commands -from .utils import scanSerial, Unit +from .utils import scanSerial, SerialStatus from .debug import debug from .async import Async diff --git a/obd/async.py b/obd/async.py index 77a00043..393c817a 100644 --- a/obd/async.py +++ b/obd/async.py @@ -31,7 +31,7 @@ import time import threading -from .utils import OBDResponse +from .OBDResponse import OBDResponse from .debug import debug from . import OBD diff --git a/obd/elm327.py b/obd/elm327.py index 5b1f8a25..1a1bf2f0 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -33,7 +33,7 @@ import serial import time from .protocols import * -from .utils import numBitsSet +from .utils import SerialStatus, numBitsSet from .debug import debug @@ -46,7 +46,7 @@ class ELM327: send_and_parse() get_port_name() - is_connected() + status() close() """ @@ -69,9 +69,9 @@ class ELM327: def __init__(self, portname, baudrate): """Initializes port by resetting device and gettings supported PIDs. """ - self.__connected = False + self.__status = SerialStatus.NOT_CONNECTED self.__port = None - self.__protocol = None + self.__protocol = UnknownProtocol self.__primary_ecu = None # message.tx_id # ------------- open port ------------- @@ -132,6 +132,7 @@ def __init__(self, portname, baudrate): self.__error("ATSPA8 did not return 'OK'") return + self.__status = SerialStatus.ELM_CONNECTED # -------------- 0100 (first command, SEARCH protocols) -------------- # TODO: rewrite this using a "wait for prompt character" @@ -169,7 +170,7 @@ def __init__(self, portname, baudrate): # ------------------------------- done ------------------------------- debug("Connection successful") - self.__connected = True + self.__status = SerialStatus.CAR_CONNECTED def __isok(self, lines, expectEcho=False): @@ -225,15 +226,16 @@ def __error(self, msg=None): if self.__port is not None: self.__port.close() - self.__connected = False + self.__status = SerialStatus.NOT_CONNECTED def get_port_name(self): return self.__port.portstr if (self.__port is not None) else "No Port" - def is_connected(self): - return self.__connected and (self.__port is not None) + @property + def status(self): + return self.__status def close(self): @@ -241,11 +243,11 @@ def close(self): Resets the device, and clears all attributes to unconnected state """ - if self.is_connected(): + if self.__status >= SerialStatus.ELM_CONNECTED: self.__write("ATZ") self.__port.close() - self.__connected = False + self.__status = SerialStatus.NOT_CONNECTED self.__port = None self.__protocol = None self.__primary_ecu = None @@ -262,7 +264,7 @@ def send_and_parse(self, cmd, delay=None): if no appropriate response was recieved. """ - if not self.is_connected(): + if not self.__status == SerialStatus.NOT_CONNECTED: debug("cannot send_and_parse() when unconnected", True) return None diff --git a/obd/obd.py b/obd/obd.py index 0923ca10..f28aae7d 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -34,7 +34,8 @@ from .__version__ import __version__ from .elm327 import ELM327 from .commands import commands -from .utils import scanSerial, OBDResponse +from .OBDResponse import OBDResponse +from .utils import scanSerial, SerialStatus from .debug import debug @@ -68,7 +69,7 @@ def __connect(self, portstr, baudrate): debug("Attempting to use port: " + str(port)) self.port = ELM327(port, baudrate) - if self.port.is_connected(): + if self.port.status >= SerialStatus.ELM_CONNECTED: # success! stop searching for serial break else: @@ -76,7 +77,7 @@ def __connect(self, portstr, baudrate): self.port = ELM327(portstr, baudrate) # if a connection was made, query for commands - if self.is_connected(): + if self.port.status == SerialStatus.CAR_CONNECTED: self.__load_commands() else: debug("Failed to connect") @@ -91,9 +92,17 @@ def close(self): self.supported_commands = [] + @property + def status(self): + if self.port is None: + return SerialStatus.NOT_CONNECTED + else: + return self.port.status + + def is_connected(self): """ Returns a boolean for whether a successful serial connection was made """ - return (self.port is not None) and self.port.is_connected() + return (self.port is not None) and (self.port.status == SerialStatus.CAR_CONNECTED) def get_port_name(self): diff --git a/obd/protocols/README.md b/obd/protocols/README.md index 15f02a18..06927df7 100644 --- a/obd/protocols/README.md +++ b/obd/protocols/README.md @@ -40,6 +40,7 @@ Inheritance structure ``` Protocol + UnknownProtocol LegacyProtocol SAE_J1850_PWM SAE_J1850_VPM diff --git a/obd/protocols/__init__.py b/obd/protocols/__init__.py index 1165e841..90976b41 100644 --- a/obd/protocols/__init__.py +++ b/obd/protocols/__init__.py @@ -29,6 +29,8 @@ # # ######################################################################## +from .protocol_unknown import UnknownProtocol + from .protocol_legacy import SAE_J1850_PWM, \ SAE_J1850_VPW, \ ISO_9141_2, \ diff --git a/obd/protocols/protocol_unknown.py b/obd/protocols/protocol_unknown.py new file mode 100644 index 00000000..26242ff8 --- /dev/null +++ b/obd/protocols/protocol_unknown.py @@ -0,0 +1,51 @@ + +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2009 Peter J. Creath # +# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# # +######################################################################## +# # +# protocols/protocol_legacy.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## + + +from .protocol import * + + +class UnknownProtocol(Protocol): + """ + Class representing an unknown protocol. + + Used for when a connection to the ELM has + been made, but the car hasn't responded. + """ + + def __init__(self): + Protocol.__init__(self) + + def create_frame(self, raw): + return Frame(raw) + + def create_message(self, frames, tx_id): + return Message(frames, tx_id) diff --git a/obd/utils.py b/obd/utils.py index c4737097..b9264a8a 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -37,6 +37,14 @@ from .debug import debug +class SerialStatus: + """ Values for the connection status flags """ + + NOT_CONNECTED = 0 + ELM_CONNECTED = 1 + CAR_CONNECTED = 2 + + def ascii_to_bytes(a): b = [] diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 198b069c..c3c19ba5 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -1,7 +1,7 @@ import obd -from obd.utils import OBDResponse -from obd.commands import OBDCommand +from obd.OBDResponse import OBDResponse +from obd.OBDCommand import OBDCommand from obd.decoders import noop from obd.protocols import SAE_J1850_PWM diff --git a/tests/test_decoders.py b/tests/test_decoders.py index d8fc9310..ae2083d7 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -1,5 +1,5 @@ -from obd.utils import Unit +from obd.OBDResponse import Unit import obd.decoders as d From df224c960ea6d201da285504eaf87334a2426fb1 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 1 Jun 2015 18:47:54 -0400 Subject: [PATCH 237/569] allow sending AT commands, fixed test --- obd/elm327.py | 6 +----- tests/test_OBD.py | 2 ++ 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 1a1bf2f0..11fa590e 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -264,14 +264,10 @@ def send_and_parse(self, cmd, delay=None): if no appropriate response was recieved. """ - if not self.__status == SerialStatus.NOT_CONNECTED: + if self.__status == SerialStatus.NOT_CONNECTED: debug("cannot send_and_parse() when unconnected", True) return None - if "AT" in cmd.upper(): - debug("Rejected sending AT command", True) - return None - lines = self.__send(cmd, delay) # parses string into list of messages diff --git a/tests/test_OBD.py b/tests/test_OBD.py index c3c19ba5..4efaa8b9 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -1,5 +1,6 @@ import obd +from obd.utils import SerialStatus from obd.OBDResponse import OBDResponse from obd.OBDCommand import OBDCommand from obd.decoders import noop @@ -31,6 +32,7 @@ def write(cmd): o.is_connected = lambda *args: True o.port.is_connected = lambda *args: True + o.port._ELM327__status = SerialStatus.CAR_CONNECTED o.port._ELM327__protocol = SAE_J1850_PWM() o.port._ELM327__primary_ecu = 0x10 o.port._ELM327__write = write From 4eb415da0e4b61a3e5832724e9836c34ec685d44 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 1 Jun 2015 19:28:18 -0400 Subject: [PATCH 238/569] moved protocol loading to a seperate function --- obd/elm327.py | 75 ++++++++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 11fa590e..05289eba 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -74,17 +74,18 @@ def __init__(self, portname, baudrate): self.__protocol = UnknownProtocol self.__primary_ecu = None # message.tx_id - # ------------- open port ------------- - debug("Opening serial port '%s'" % portname) + # ------------- open port ------------- try: + debug("Opening serial port '%s'" % portname) self.__port = serial.Serial(portname, \ baudrate = baudrate, \ parity = serial.PARITY_NONE, \ stopbits = 1, \ bytesize = 8, \ timeout = 3) # seconds + debug("Serial port successfully opened on " + self.get_port_name()) except serial.SerialException as e: self.__error(e) @@ -93,7 +94,6 @@ def __init__(self, portname, baudrate): self.__error(e) return - debug("Serial port successfully opened on " + self.get_port_name()) # ---------------------------- ATZ (reset) ---------------------------- @@ -104,35 +104,45 @@ def __init__(self, portname, baudrate): self.__error(e) return - # -------------------------- ATE0 (echo OFF) -------------------------- r = self.__send("ATE0") if not self.__isok(r, expectEcho=True): self.__error("ATE0 did not return 'OK'") return - # ------------------------- ATH1 (headers ON) ------------------------- r = self.__send("ATH1") if not self.__isok(r): self.__error("ATH1 did not return 'OK', or echoing is still ON") return - # ------------------------ ATL0 (linefeeds OFF) ----------------------- r = self.__send("ATL0") if not self.__isok(r): self.__error("ATL0 did not return 'OK'") return - # ---------------------- ATSPA8 (protocol AUTO) ----------------------- r = self.__send("ATSPA8") if not self.__isok(r): self.__error("ATSPA8 did not return 'OK'") return - self.__status = SerialStatus.ELM_CONNECTED + + + # try to communicate with the car, and load the correct protocol parser + if self.load_protocol(): + self.__status = SerialStatus.CAR_CONNECTED + else: + self.__status = SerialStatus.ELM_CONNECTED + + + + # ------------------------------- done ------------------------------- + debug("Connection successful") + + + def load_protocol(self): # -------------- 0100 (first command, SEARCH protocols) -------------- # TODO: rewrite this using a "wait for prompt character" @@ -144,8 +154,8 @@ def __init__(self, portname, baudrate): r = self.__send("ATDPN") if not r: - self.__error("Describe protocol command didn't return ") - return + debug("Describe protocol command didn't return", True) + return False p = r[0] @@ -153,8 +163,8 @@ def __init__(self, portname, baudrate): p = p[1:] if (len(p) > 1 and p.startswith("A")) else p[:-1] if p not in self._SUPPORTED_PROTOCOLS: - self.__error("ELM responded with unknown protocol") - return + debug("ELM responded with unknown protocol", True) + return False # instantiate the correct protocol handler self.__protocol = self._SUPPORTED_PROTOCOLS[p]() @@ -163,14 +173,12 @@ def __init__(self, portname, baudrate): # which ECU is the primary. m = self.__protocol(r0100) - self.__primary_ecu = self.__find_primary_ecu(m) + self.__primary_ecu = find_primary_ecu(m) if self.__primary_ecu is None: - self.__error("Failed to choose primary ECU") - return + debug("Failed to choose primary ECU", True) + return False - # ------------------------------- done ------------------------------- - debug("Connection successful") - self.__status = SerialStatus.CAR_CONNECTED + return True def __isok(self, lines, expectEcho=False): @@ -182,6 +190,20 @@ def __isok(self, lines, expectEcho=False): return len(lines) == 1 and lines[0] == 'OK' + def __error(self, msg=None): + """ handles fatal failures, print debug info and closes serial """ + + debug("Connection Error:", True) + + if msg is not None: + debug(' ' + str(msg), True) + + if self.__port is not None: + self.__port.close() + + self.__status = SerialStatus.NOT_CONNECTED + + def __find_primary_ecu(self, messages): """ Given a list of messages from different ECUS, @@ -215,20 +237,6 @@ def __find_primary_ecu(self, messages): return tx_id - def __error(self, msg=None): - """ handles fatal failures, print debug info and closes serial """ - - debug("Connection Error:", True) - - if msg is not None: - debug(' ' + str(msg), True) - - if self.__port is not None: - self.__port.close() - - self.__status = SerialStatus.NOT_CONNECTED - - def get_port_name(self): return self.__port.portstr if (self.__port is not None) else "No Port" @@ -287,7 +295,8 @@ def __send(self, cmd, delay=None): unprotected send() function will __write() the given string, no questions asked. - returns result of __read() after an optional delay. + returns result of __read() (a list of line strings) + after an optional delay. """ self.__write(cmd) From 7e83f4da4bb121e1f68fc153638a71fd02ea1d75 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 1 Jun 2015 23:02:53 -0400 Subject: [PATCH 239/569] wrote ECU tagging, moved fallback handlers to Protocol, and simplified query() --- obd/OBDCommand.py | 4 +- obd/elm327.py | 55 +------- obd/obd.py | 53 ++++---- obd/protocols/README.md | 31 +++-- obd/protocols/__init__.py | 2 + obd/protocols/protocol.py | 204 ++++++++++++++++++++++++------ obd/protocols/protocol_can.py | 80 ++++++------ obd/protocols/protocol_legacy.py | 66 +++++----- obd/protocols/protocol_unknown.py | 8 +- tests/test_OBD.py | 2 +- tests/test_OBDCommand.py | 3 +- tests/test_elm327.py | 22 ++-- tests/test_protocol_can.py | 12 +- tests/test_protocol_legacy.py | 12 +- 14 files changed, 329 insertions(+), 225 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index b88c857f..b5674b3f 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -64,6 +64,8 @@ def get_pid_int(self): def __call__(self, message): + # TODO: handle list of messages as parameter + # create the response object with the raw data recieved # and reference to original command r = OBDResponse(self, message) @@ -72,7 +74,7 @@ def __call__(self, message): # TODO: rewrite decoders to handle raw byte arrays _data = "" - for b in message.data_bytes: + for b in message.data: h = hex(b)[2:].upper() h = "0" + h if len(h) < 2 else h _data += h diff --git a/obd/elm327.py b/obd/elm327.py index 05289eba..ef19af3f 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -204,39 +204,6 @@ def __error(self, msg=None): self.__status = SerialStatus.NOT_CONNECTED - def __find_primary_ecu(self, messages): - """ - Given a list of messages from different ECUS, - (in response to the 0100 PID listing command) - choose the ID of the primary ECU - """ - - if len(messages) == 0: - return None - elif len(messages) == 1: - return messages[0].tx_id - else: - # first, try filtering for the standard ECU IDs - test = lambda m: m.tx_id == self.__protocol.PRIMARY_ECU - - if bool([m for m in messages if test(m)]): - return self.__protocol.PRIMARY_ECU - else: - # last resort solution, choose ECU - # with the most PIDs supported - best = 0 - tx_id = None - - for message in messages: - bits = sum([numBitsSet(b) for b in message.data_bytes]) - - if bits > best: - best = bits - tx_id = message.tx_id - - return tx_id - - def get_port_name(self): return self.__port.portstr if (self.__port is not None) else "No Port" @@ -261,33 +228,23 @@ def close(self): self.__primary_ecu = None - def send_and_parse(self, cmd, delay=None): + def send_and_parse(self, cmd): """ send() function used to service all OBDCommands - Sends the given command string (rejects "AT" command), - parses the response string with the appropriate protocol object. + Sends the given command string, and parses the + response lines with the protocol object. - Returns the Message object from the primary ECU, or None, - if no appropriate response was recieved. + Returns a list of Message objects """ if self.__status == SerialStatus.NOT_CONNECTED: debug("cannot send_and_parse() when unconnected", True) return None - lines = self.__send(cmd, delay) - - # parses string into list of messages + lines = self.__send(cmd) messages = self.__protocol(lines) - - # select the first message with the ECU ID we're looking for - # TODO: use ELM header settings to query ECU by address directly - for message in messages: - if message.tx_id == self.__primary_ecu: - return message - - return None # no suitable response was returned + return messages def __send(self, cmd, delay=None): diff --git a/obd/obd.py b/obd/obd.py index f28aae7d..d2f3ba33 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -85,7 +85,7 @@ def __connect(self, portstr, baudrate): def close(self): """ Closes the connection """ - if self.is_connected(): + if self.status != SerialStatus.NOT_CONNECTED: debug("Closing connection") self.port.close() self.port = None @@ -102,12 +102,12 @@ def status(self): def is_connected(self): """ Returns a boolean for whether a successful serial connection was made """ - return (self.port is not None) and (self.port.status == SerialStatus.CAR_CONNECTED) + return self.status == SerialStatus.CAR_CONNECTED def get_port_name(self): """ Returns the name of the currently connected port """ - if self.is_connected(): + if self.status != SerialStatus.NOT_CONNECTED: return self.port.get_port_name() else: return "Not connected to any port" @@ -131,7 +131,7 @@ def __load_commands(self): if not self.supports(get): continue - response = self.__send(get) # ask nicely + response = self.query(get, force=True) # ask nicely if response.is_null(): continue @@ -165,41 +165,36 @@ def print_commands(self): print(str(c)) - def supports(self, c): + def supports(self, cmd): """ Returns a boolean for whether the car supports the given command """ - return commands.has_command(c) and c.supported + return commands.has_command(cmd) and cmd.supported - def __send(self, c): + def query(self, cmd, force=False): """ - Back-end implementation of query() - sends the given command, retrieves and parses the response + primary API function. Sends commands to the car, and + protects against sending unsupported commands. """ - if not self.is_connected(): + if self.status == SerialStatus.NOT_CONNECTED: debug("Query failed, no connection available", True) - return OBDResponse() # return empty response + return OBDResponse() - debug("Sending command: %s" % str(c)) + if not self.supports(cmd) and not force: + debug("'%s' is not supported" % str(cmd), True) + return OBDResponse() # send command and retrieve message - m = self.port.send_and_parse(c.get_command()) + debug("Sending command: %s" % str(cmd)) + messages = self.port.send_and_parse(cmd.get_command()) - if m is None: - return OBDResponse() # return empty response - else: - return c(m) # compute a response object + if not messages: + debug("No valid OBD Messages returned", True) + return OBDResponse() + # select the first message with the ECU ID we're looking for + for message in messages: + if message.tx_id == self.__primary_ecu: + return message - def query(self, c, force=False): - """ - primary API function. Sends commands to the car, and - protects against sending unsupported commands. - """ - - # check that the command is supported - if self.supports(c) or force: - return self.__send(c) - else: - debug("'%s' is not supported" % str(c), True) - return OBDResponse() # return empty response + return cmd(messages) # compute a response object diff --git a/obd/protocols/README.md b/obd/protocols/README.md index 06927df7..5d7d5a8b 100644 --- a/obd/protocols/README.md +++ b/obd/protocols/README.md @@ -1,11 +1,11 @@ Notes ----- -Each protocol object is callable, and accepts a list of raw input strings, and returns a list of parsed `Message` objects. The `data_bytes` field will contain a list of integers, corresponding to all relevant data returned by the command. +Each protocol object is callable, and accepts a list of raw input strings, and returns a list of parsed `Message` objects. The `data` field will contain a list of integers, corresponding to all relevant data returned by the command. -*Note: `Message.data_bytes` does not refer to the full data field of a message, but rather a subset of this field. Things like Mode/PID/PCI bytes are removed. However, `Frame.data_bytes` DOES include the full data field (per-spec), for each frame.* +*Note: `Message.data` does not refer to the full data field of a message, but rather a subset of this field. Things like Mode/PID/PCI bytes are removed. However, `Frame.data_bytes` DOES include the full data field (per-spec), for each frame.* -For example, these are the resultant `Message.data_bytes` fields for some single frame messages: +For example, these are the resultant `Message.data` fields for some single frame messages: ``` A CAN Message: @@ -17,22 +17,37 @@ A J1850 Message: [ data ] ``` +The parsing itself (invoking `__call__`) is stateless. The only stateful part of a `Protocol` is the `ECU_Map`. These objects correlate OBD transmitter IDs (`tx_id`'s) with the various ECUs in the car. This way, `Message` objects can be marked with ECU constants such as: + +- ENGINE +- TRANSMISSION + +Ideally they'd be constant across all protocols and vehicles, but, they're aren't. To help quell the madness, each protocol can define default `tx_id`'s for various ECUs. When `Protocol` objects are constructed, they accept a raw OBD response (from a 0100 command) to check these mappings. If the engine ECU can't be identified, there's fallback logic to select its `tx_id` from the 0100 response. + Subclassing `Protocol` --------------------- -All protocol objects must implement two functions: +All protocol objects must implement the following: + +---------------------------------------- + +#### parse_frame(self, frame) + +Recieves a single `Frame` object with `Frame.raw` preloaded with the raw line recieved from the car (in string form). This function is responsible for parsing `Frame.raw`, and filling the remaining fields in the `Frame` object. If the frame is invalid, or the parse fails, this function should return `False`, and the frame will be dropped. ---------------------------------------- -#### create_frame(self, raw) +#### parse_message(self, message) -Recieves a single frame (in string form), and is responsible for parsing and returning a new `Frame` object. If the frame is invalid, or the parse fails, this function should return `None`, and the frame will be dropped. +Recieves a single `Message` object with `Message.frames` preloaded with a list of `Frame` objects. This function is responsible for assembling the frames into the `Frame.data` field in the `Message` object. This is where multi-line responses are assembled. If the message is found to be invalid, this function should return `False`, and the entire message will be dropped. ---------------------------------------- -#### create_message(self, frames, tx_id) +#### Normal TX_ID's + +Each protocol has a different way of notating the ID of the transmitter, so each subclass must set its own attributes denoting standard `tx_id`'s. Refer to the base `Protocol` class for a list of these attributes. Currently, they are: -Recieves a list of `Frame`s, and is responsible for assembling them into a finished `Message` object. This is where multi-line responses are assembled, and the final `Message.data_bytes` field is filled. If the message is found to be invalid, this function should return `None`, and the entire message will be dropped. +- `TX_ID_ENGINE` Inheritance structure diff --git a/obd/protocols/__init__.py b/obd/protocols/__init__.py index 90976b41..5460dcf4 100644 --- a/obd/protocols/__init__.py +++ b/obd/protocols/__init__.py @@ -29,6 +29,8 @@ # # ######################################################################## +from .protocol import ECU + from .protocol_unknown import UnknownProtocol from .protocol_legacy import SAE_J1850_PWM, \ diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index e38f75f8..c076a169 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -29,7 +29,7 @@ # # ######################################################################## -from obd.utils import ascii_to_bytes, isHex +from obd.utils import ascii_to_bytes, isHex, numBitsSet from obd.debug import debug @@ -40,28 +40,91 @@ """ +class ECU: + """ constant flags used for marking and filtering messages """ + + ANY = 0b11111111 # used by OBDCommands to accept messages from any ECU + UNKNOWN = 0b00000000 + + ENGINE = 0b00000001 # each ECU gets its own bit for ease of making OR filters + # TRANSMISSION = 0b00000010 + + +class ECU_Map: + """ correlation of tx_id to ECU constants above """ + + def __init__(self, init_map): + self.forward_map = init_map # tx_id ---> ECU ID + self.backward_map = {} # ECU ID ---> tx_id + + # the backwards map is simply used to check for ECU ID collisions + # since it shouldn't be possible to have two tx_id's represent the engine + + # construct the backwards map + for key in self.forward_map: + value = self.forward_map[key] + self.backward_map[value] = key + + def set(self, tx_id, ecu_id): + """ maps a tx_id to an ECU ID, and remove any old mappings to that ECU ID """ + + # check the backwards map to see if this ECU ID was already registered + if ecu_id in self.backward_map: + # if so, unregister the old mapping + old_tx_id = self.backward_map[ecu_id] + del self.forward_map[old_tx_id] + del self.backward_map[ecu_id] + + # record the new mapping + self.forward_map[tx_id] = ecu_id + self.backward_map[ecu_id] = tx_id + + def resolve(self, tx_id): + """ converts a tx_id into an ECU ID constant """ + if tx_id in self.forward_map: + return self.forward_map[tx_id] + else: + return ECU.UNKNOWN + + def lookup(self, ecu_id): + """ converts an ECU ID constant into a tx_id (mostly for testing) """ + if ecu_id in self.backward_map: + return self.backward_map[ecu_id] + else: + return None + + + class Frame(object): def __init__(self, raw): - self.raw = raw - self.data_bytes = [] - self.priority = None - self.addr_mode = None - self.rx_id = None - self.tx_id = None - self.type = None - self.seq_index = 0 # only used when type = CF - self.data_len = None + self.raw = raw + self.data = [] + self.priority = None + self.addr_mode = None + self.rx_id = None + self.tx_id = None + self.type = None + self.seq_index = 0 # only used when type = CF + self.data_len = None class Message(object): - def __init__(self, frames, tx_id): - self.frames = frames - self.tx_id = tx_id - self.data_bytes = [] + def __init__(self, raw, frames): + self.raw = raw + self.frames = frames + self.ecu = ECU.UNKNOWN + self.data = [] + + @property + def tx_id(self): + if len(self.frames) == 0: + return None + else: + return self.frames[0].tx_id def __eq__(self, other): if isinstance(other, Message): - for attr in ["frames", "tx_id", "data_bytes"]: + for attr in ["raw", "frames", "ecu", "data"]: if getattr(self, attr) != getattr(other, attr): return False return True @@ -81,30 +144,59 @@ def __eq__(self, other): class Protocol(object): - PRIMARY_ECU = None + # override in subclass for each protocol + TX_ID_ENGINE = None + + + def __init__(self, lines_0100): + """ + constructs a protocol object + + uses a list of raw strings from the + car to determine the ECU layout. + """ - def __init__(self, baud=38400): - self.baud = baud + # create the default map + self.ecu_map = ECU_Map({ + self.TX_ID_ENGINE : ECU.ENGINE + }) + + # parse the 0100 data into messages + # NOTE: at this point, their "ecu" property will be UKNOWN + messages = self(lines_0100) + + # read the messages and assemble the map + # subsequent runs will now be tagged correctly + self.populate_ecu_map(messages) def __call__(self, lines): + """ + Main function + + accepts a list of raw strings from the car, split by lines + """ # ditch spaces - lines = [line.replace(' ', '') for line in lines] + filtered_lines = [line.replace(' ', '') for line in lines] # ditch frames without valid hex (trashes "NO DATA", etc...) - lines = filter(isHex, lines) + filtered_lines = filter(isHex, filtered_lines) + # parse each frame (each line) frames = [] - for line in lines: - # subclass function to parse the lines into Frames - frame = self.create_frame(line) + for line in filtered_lines: + frame = Frame(line) + + # subclass function to parse the lines into Frames # drop frames that couldn't be parsed - if frame is not None: + if self.parse_frame(frame): frames.append(frame) - # group frames by transmitting ECU (tx_id) + + # group frames by transmitting ECU + # ecus[tx_id] = [Frame, Frame] ecus = {} for frame in frames: if frame.tx_id not in ecus: @@ -112,37 +204,75 @@ def __call__(self, lines): else: ecus[frame.tx_id].append(frame) + # parse frames into whole messages messages = [] for ecu in ecus: - # subclass function to assemble frames into Messages - message = self.create_message(ecus[ecu], ecu) - # drop messages that couldn't be assembled - if message is not None: + # new message object with a copy of the raw data + # and frames addressed for this ecu + message = Message(list(lines), ecus[ecu]) + + # subclass function to assemble frames into Messages + if self.parse_message(message): messages.append(message) + message.ecu = self.ecu_map.resolve(ecu) # mark with the appropriate ECU ID return messages - def create_frame(self, raw): + def populate_ecu_map(self, messages): + """ + Given a list of messages from different ECUS, + (in response to the 0100 PID listing command) + associate each tx_id to an ECU ID constant + """ + + if len(messages) == 0: + pass + elif len(messages) == 1: + # if there's only one response, mark it as the engine regardless + self.ecu_map.set(messages[0].tx_id, ECU.ENGINE) + else: + + # if none of the messages correspond to the engine, + test = lambda m: m.tx_id == self.TX_ID_ENGINE + if not bool([m for m in messages if test(m)]): + # last resort solution, choose ECU + # with the most bits set (most PIDs supported) + best = 0 + tx_id = None + + for message in messages: + bits = sum([numBitsSet(b) for b in message.data]) + + if bits > best: + best = bits + tx_id = message.tx_id + + self.ecu_map.set(tx_id, ECU.ENGINE) + + + def parse_frame(self, frame): """ override in subclass for each protocol - Function recieves a list of byte values for a frame. + Function recieves a Frame object preloaded + with the raw string line from the car. - Function should return a Frame instance. If fatal errors were - found, this function should return None (the Frame is dropped). + Function should return a boolean. If fatal errors were + found, this function should return False (the Frame is dropped). """ raise NotImplementedError() - def create_message(self, frames): + def parse_message(self, message): """ override in subclass for each protocol - Function recieves a list of Frame objects. + Function recieves a Message object + preloaded with a list of Frame objects. - Function should return a Message instance. If fatal errors were - found, this function should return None (the Message is dropped). + Function should return a boolean. If fatal errors were + found, this function should return False (the Message is dropped). """ raise NotImplementedError() diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 890fa6d6..738c5bc1 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -35,25 +35,27 @@ class CANProtocol(Protocol): - PRIMARY_ECU = 0 + TX_ID_ENGINE = 0 FRAME_TYPE_SF = 0x00 # single frame FRAME_TYPE_FF = 0x10 # first frame of multi-frame message FRAME_TYPE_CF = 0x20 # consecutive frame(s) of multi-frame message - def __init__(self, baud, id_bits): - Protocol.__init__(self, baud) + def __init__(self, lines_0100, id_bits): + Protocol.__init__(self, lines_0100) self.id_bits = id_bits - def create_frame(self, raw): + + def parse_frame(self, frame): + + raw = frame.raw # pad 11-bit CAN headers out to 32 bits for consistency, # since ELM already does this for 29-bit CAN headers if self.id_bits == 11: raw = "00000" + raw - frame = Frame(raw) raw_bytes = ascii_to_bytes(raw) # read header information @@ -87,44 +89,44 @@ def create_frame(self, raw): # [ Frame ] # 00 00 07 E8 06 41 00 BE 7F B8 13 - frame.data_bytes = raw_bytes[4:] + frame.data = raw_bytes[4:] # read PCI byte (always first byte in the data section) - frame.type = frame.data_bytes[0] & 0xF0 + frame.type = frame.data[0] & 0xF0 if frame.type not in [self.FRAME_TYPE_SF, self.FRAME_TYPE_FF, self.FRAME_TYPE_CF]: debug("Dropping frame carrying unknown PCI frame type") - return None + return False if frame.type == self.FRAME_TYPE_SF: # single frames have 4 bit length codes - frame.data_len = frame.data_bytes[0] & 0x0F + frame.data_len = frame.data[0] & 0x0F elif frame.type == self.FRAME_TYPE_FF: # First frames have 12 bit length codes - frame.data_len = (frame.data_bytes[0] & 0x0F) << 8 - frame.data_len += frame.data_bytes[1] + frame.data_len = (frame.data[0] & 0x0F) << 8 + frame.data_len += frame.data[1] elif frame.type == self.FRAME_TYPE_CF: # Consecutive frames have 4 bit sequence indices - frame.seq_index = frame.data_bytes[0] & 0x0F + frame.seq_index = frame.data[0] & 0x0F - return frame + return True - def create_message(self, frames, tx_id): + def parse_message(self, message): - message = Message(frames, tx_id) + frames = message.frames - if len(message.frames) == 1: + if len(frames) == 1: frame = frames[0] if frame.type != self.FRAME_TYPE_SF: debug("Recieved lone frame not marked as single frame") - return None + return False # extract data, ignore PCI byte and anything after the marked length - message.data_bytes = frame.data_bytes[1:1+frame.data_len] + message.data = frame.data[1:1+frame.data_len] else: # sort FF and CF into their own lists @@ -143,15 +145,15 @@ def create_message(self, frames, tx_id): # check that we captured only one first-frame if len(ff) > 1: debug("Recieved multiple frames marked FF") - return None + return False elif len(ff) == 0: debug("Never received frame marked FF") - return None + return False # check that there was at least one consecutive-frame if len(cf) == 0: debug("Never received frame marked CF") - return None + return False # calculate proper sequence indices from the lower 4 bits given for prev, curr in zip(cf, cf[1:]): @@ -174,32 +176,32 @@ def create_message(self, frames, tx_id): indices = [f.seq_index for f in cf] if not contiguous(indices, 1, len(cf)): debug("Recieved multiline response with missing frames") - return None + return False # on the first frame, skip PCI byte AND length code - message.data_bytes += ff[0].data_bytes[2:] + message.data += ff[0].data[2:] # now that they're in order, load/accumulate the data from each CF frame for f in cf: - message.data_bytes += f.data_bytes[1:] # chop off the PCI byte + message.data += f.data[1:] # chop off the PCI byte # chop off the Mode/PID bytes based on the mode number - mode = message.data_bytes[0] + mode = message.data[0] if mode == 0x43: # fetch the DTC count, and use it as a length code - num_dtc_bytes = message.data_bytes[1] * 2 + num_dtc_bytes = message.data[1] * 2 # skip the PID byte and the DTC count, - message.data_bytes = message.data_bytes[2:][:num_dtc_bytes] + message.data = message.data[2:][:num_dtc_bytes] else: # handles cases when there is both a Mode and PID byte - message.data_bytes = message.data_bytes[2:] + message.data = message.data[2:] - return message + return True ############################################## @@ -211,25 +213,25 @@ def create_message(self, frames, tx_id): class ISO_15765_4_11bit_500k(CANProtocol): - def __init__(self): - CANProtocol.__init__(self, baud=500000, id_bits=11) + def __init__(self, lines_0100): + CANProtocol.__init__(self, lines_0100, id_bits=11) class ISO_15765_4_29bit_500k(CANProtocol): - def __init__(self): - CANProtocol.__init__(self, baud=500000, id_bits=29) + def __init__(self, lines_0100): + CANProtocol.__init__(self, lines_0100, id_bits=29) class ISO_15765_4_11bit_250k(CANProtocol): - def __init__(self): - CANProtocol.__init__(self, baud=250000, id_bits=11) + def __init__(self, lines_0100): + CANProtocol.__init__(self, lines_0100, id_bits=11) class ISO_15765_4_29bit_250k(CANProtocol): - def __init__(self): - CANProtocol.__init__(self, baud=250000, id_bits=29) + def __init__(self, lines_0100): + CANProtocol.__init__(self, lines_0100, id_bits=29) class SAE_J1939(CANProtocol): - def __init__(self): - CANProtocol.__init__(self, baud=250000, id_bits=29) + def __init__(self, lines_0100): + CANProtocol.__init__(self, lines_0100, id_bits=29) diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 1de6378f..e6c34915 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -35,23 +35,26 @@ class LegacyProtocol(Protocol): - PRIMARY_ECU = 0x10 + TX_ID_ENGINE = 0x10 - def __init__(self, baud): - Protocol.__init__(self, baud) - def create_frame(self, raw): + def __init__(self, lines_0100): + Protocol.__init__(self, lines_0100) + + + def parse_frame(self, frame): + + raw = frame.raw - frame = Frame(raw) raw_bytes = ascii_to_bytes(raw) if len(raw_bytes) < 6: debug("Dropped frame for being too short") - return None + return False if len(raw_bytes) > 11: debug("Dropped frame for being too long") - return None + return False # Ex. # [Header] [ Frame ] @@ -59,27 +62,28 @@ def create_frame(self, raw): # ck = checksum byte # exclude header and trailing checksum (handled by ELM adapter) - frame.data_bytes = raw_bytes[3:-1] + frame.data = raw_bytes[3:-1] # read header information frame.priority = raw_bytes[0] frame.rx_id = raw_bytes[1] frame.tx_id = raw_bytes[2] - return frame + return True + - def create_message(self, frames, tx_id): + def parse_message(self, message): - message = Message(frames, tx_id) + frames = message.frames # len(frames) will always be >= 1 (see the caller, protocol.py) - mode = frames[0].data_bytes[0] + mode = frames[0].data[0] # test that all frames are responses to the same Mode (SID) if len(frames) > 1: - if not all([mode == f.data_bytes[0] for f in frames[1:]]): + if not all([mode == f.data[0] for f in frames[1:]]): debug("Recieved frames from multiple commands") - return None + return False # legacy protocols have different re-assembly # procedures for different Modes @@ -95,7 +99,7 @@ def create_message(self, frames, tx_id): # [ Data ] for f in frames: - message.data_bytes += f.data_bytes[1:] + message.data += f.data[1:] else: if len(frames) == 1: @@ -106,7 +110,7 @@ def create_message(self, frames, tx_id): # 48 6B 10 41 00 BE 7F B8 13 ck # [ Data ] - message.data_bytes = frames[0].data_bytes[2:] + message.data = frames[0].data[2:] else: # len(frames) > 1: # generic multiline requests carry an order byte @@ -119,19 +123,19 @@ def create_message(self, frames, tx_id): # etc... [] [ Data ] # sort the frames by the order byte - frames = sorted(frames, key=lambda f: f.data_bytes[2]) + frames = sorted(frames, key=lambda f: f.data[2]) # check contiguity - indices = [f.data_bytes[2] for f in frames] + indices = [f.data[2] for f in frames] if not contiguous(indices, 1, len(frames)): debug("Recieved multiline response with missing frames") - return None + return False # now that they're in order, accumulate the data from each frame for f in frames: - message.data_bytes += f.data_bytes[3:] # loose the mode/pid/seq bytes + message.data += f.data[3:] # loose the mode/pid/seq bytes - return message + return True @@ -144,25 +148,25 @@ def create_message(self, frames, tx_id): class SAE_J1850_PWM(LegacyProtocol): - def __init__(self): - LegacyProtocol.__init__(self, baud=41600) + def __init__(self, lines_0100): + LegacyProtocol.__init__(self, lines_0100) class SAE_J1850_VPW(LegacyProtocol): - def __init__(self): - LegacyProtocol.__init__(self, baud=10400) + def __init__(self, lines_0100): + LegacyProtocol.__init__(self, lines_0100) class ISO_9141_2(LegacyProtocol): - def __init__(self): - LegacyProtocol.__init__(self, baud=10400) + def __init__(self, lines_0100): + LegacyProtocol.__init__(self, lines_0100) class ISO_14230_4_5baud(LegacyProtocol): - def __init__(self): - LegacyProtocol.__init__(self, baud=10400) + def __init__(self, lines_0100): + LegacyProtocol.__init__(self, lines_0100) class ISO_14230_4_fast(LegacyProtocol): - def __init__(self): - LegacyProtocol.__init__(self, baud=10400) + def __init__(self, lines_0100): + LegacyProtocol.__init__(self, lines_0100) diff --git a/obd/protocols/protocol_unknown.py b/obd/protocols/protocol_unknown.py index 26242ff8..faa4836c 100644 --- a/obd/protocols/protocol_unknown.py +++ b/obd/protocols/protocol_unknown.py @@ -44,8 +44,8 @@ class UnknownProtocol(Protocol): def __init__(self): Protocol.__init__(self) - def create_frame(self, raw): - return Frame(raw) + def parse_frame(self, raw): + return False - def create_message(self, frames, tx_id): - return Message(frames, tx_id) + def parse_message(self, frames, tx_id): + return False diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 4efaa8b9..b94fb0dd 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -33,7 +33,7 @@ def write(cmd): o.is_connected = lambda *args: True o.port.is_connected = lambda *args: True o.port._ELM327__status = SerialStatus.CAR_CONNECTED - o.port._ELM327__protocol = SAE_J1850_PWM() + o.port._ELM327__protocol = SAE_J1850_PWM([]) o.port._ELM327__primary_ecu = 0x10 o.port._ELM327__write = write o.port._ELM327__read = lambda *args: fromCar diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index d1788cb8..e049e2a7 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -39,7 +39,7 @@ def test_clone(): def test_call(): - p = SAE_J1850_PWM() + p = SAE_J1850_PWM([]) m = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) # parse valid data into response object # valid response size @@ -77,4 +77,3 @@ def test_get_pid_int(): cmd = OBDCommand("", "", "01", "", 4, noop) assert cmd.get_pid_int() == 0 - diff --git a/tests/test_elm327.py b/tests/test_elm327.py index 83c0ed4a..dac54503 100644 --- a/tests/test_elm327.py +++ b/tests/test_elm327.py @@ -1,25 +1,23 @@ -from obd.protocols import SAE_J1850_PWM +from obd.protocols import ECU, SAE_J1850_PWM from obd.elm327 import ELM327 def test_find_primary_ecu(): # parse from messages - p = ELM327("/dev/null", 38400) # pyserial will yell, but this isn't testing tx/rx - p._ELM327__protocol = SAE_J1850_PWM() - # use primary ECU when multiple are present - m = p._ELM327__protocol(["48 6B 10 41 00 BE 1F B8 11 AA", "48 6B 12 41 00 BE 1F B8 11 AA"]) - assert p._ELM327__find_primary_ecu(m) == 0x10 + p = SAE_J1850_PWM(["48 6B 10 41 00 BE 1F B8 11 AA", "48 6B 12 41 00 BE 1F B8 11 AA"]) + assert p.ecu_map.lookup(ECU.ENGINE) == 0x10 # use lone responses regardless - m = p._ELM327__protocol(["48 6B 12 41 00 BE 1F B8 11 AA"]) - assert p._ELM327__find_primary_ecu(m) == 0x12 + p = SAE_J1850_PWM(["48 6B 12 41 00 BE 1F B8 11 AA"]) + assert p.ecu_map.lookup(ECU.ENGINE) == 0x12 # if primary ECU is not listed, use response with most PIDs supported - m = p._ELM327__protocol(["48 6B 12 41 00 BE 1F B8 11 AA", "48 6B 14 41 00 00 00 B8 11 AA"]) - assert p._ELM327__find_primary_ecu(m) == 0x12 + p = SAE_J1850_PWM(["48 6B 12 41 00 BE 1F B8 11 AA", "48 6B 14 41 00 00 00 B8 11 AA"]) + assert p.ecu_map.lookup(ECU.ENGINE) == 0x12 - # if no messages were received, no ECU could be determined - assert p._ELM327__find_primary_ecu([]) == None + # if no messages were received, the defaults stay in place + p = SAE_J1850_PWM([]) + assert p.ecu_map.lookup(ECU.ENGINE) == p.TX_ID_ENGINE diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index 8799f1f6..3601049b 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -19,11 +19,11 @@ ] -def check_message(m, num_frames, tx_id, data_bytes): +def check_message(m, num_frames, tx_id, data): """ generic test for correct message values """ assert len(m.frames) == num_frames assert m.tx_id == tx_id - assert m.data_bytes == data_bytes + assert m.data == data @@ -31,7 +31,7 @@ def check_message(m, num_frames, tx_id, data_bytes): def test_single_frame(): for protocol in CAN_11_PROTOCOLS: - p = protocol() + p = protocol([]) r = p(["7E8 06 41 00 00 01 02 03"]) @@ -42,7 +42,7 @@ def test_single_frame(): def test_hex_straining(): for protocol in CAN_11_PROTOCOLS: - p = protocol() + p = protocol([]) r = p(["NO DATA"]) @@ -62,7 +62,7 @@ def test_hex_straining(): def test_multi_ecu(): for protocol in CAN_11_PROTOCOLS: - p = protocol() + p = protocol([]) test_case = [ @@ -87,7 +87,7 @@ def test_multi_ecu(): def test_multi_line(): for protocol in CAN_11_PROTOCOLS: - p = protocol() + p = protocol([]) test_case = [ diff --git a/tests/test_protocol_legacy.py b/tests/test_protocol_legacy.py index f828967a..71cfb8a4 100644 --- a/tests/test_protocol_legacy.py +++ b/tests/test_protocol_legacy.py @@ -16,16 +16,16 @@ ] -def check_message(m, num_frames, tx_id, data_bytes): +def check_message(m, num_frames, tx_id, data): """ generic test for correct message values """ assert len(m.frames) == num_frames assert m.tx_id == tx_id - assert m.data_bytes == data_bytes + assert m.data == data def test_single_frame(): for protocol in LEGACY_PROTOCOLS: - p = protocol() + p = protocol([]) # minimum valid length r = p(["48 6B 10 41 00 FF"]) @@ -48,7 +48,7 @@ def test_single_frame(): def test_hex_straining(): for protocol in LEGACY_PROTOCOLS: - p = protocol() + p = protocol([]) r = p(["NO DATA"]) @@ -68,7 +68,7 @@ def test_hex_straining(): def test_multi_ecu(): for protocol in LEGACY_PROTOCOLS: - p = protocol() + p = protocol([]) test_case = [ @@ -92,7 +92,7 @@ def test_multi_ecu(): def test_multi_line(): for protocol in LEGACY_PROTOCOLS: - p = protocol() + p = protocol([]) test_case = [ From e7ab03f0b10720a3e2cecac130d85e62475a4c61 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 2 Jun 2015 00:25:36 -0400 Subject: [PATCH 240/569] OBDCommands now take multiple messages as arguments --- obd/OBDCommand.py | 32 +++-- obd/commands.py | 215 +++++++++++++++--------------- obd/elm327.py | 13 +- obd/obd.py | 5 - obd/protocols/protocol.py | 2 +- obd/protocols/protocol_can.py | 2 +- obd/protocols/protocol_unknown.py | 11 +- 7 files changed, 134 insertions(+), 146 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index b5674b3f..77141f5a 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -29,20 +29,20 @@ # # ######################################################################## -import re from .utils import * from .debug import debug from .OBDResponse import OBDResponse class OBDCommand(): - def __init__(self, name, desc, mode, pid, returnBytes, decoder, supported=False): + def __init__(self, name, desc, mode, pid, returnBytes, decoder, ecu, supported=False): self.name = name self.desc = desc self.mode = mode self.pid = pid self.bytes = returnBytes # number of bytes expected in return self.decode = decoder + self.ecu = ecu self.supported = supported def clone(self): @@ -51,7 +51,8 @@ def clone(self): self.mode, self.pid, self.bytes, - self.decode) + self.decode, + self.ecu) def get_command(self): return self.mode + self.pid # the actual command transmitted to the port @@ -62,26 +63,29 @@ def get_mode_int(self): def get_pid_int(self): return unhex(self.pid) - def __call__(self, message): - - # TODO: handle list of messages as parameter + def __call__(self, messages): # create the response object with the raw data recieved # and reference to original command - r = OBDResponse(self, message) + r = OBDResponse(self, messages) # combine the bytes back into a hex string # TODO: rewrite decoders to handle raw byte arrays _data = "" - for b in message.data: - h = hex(b)[2:].upper() - h = "0" + h if len(h) < 2 else h - _data += h + # filter for applicable messages + for message in messages: + + # if this command accepts messages from this ECU + if self.ecu & message.ecu > 0: + for b in message.data: + h = hex(b)[2:].upper() + h = "0" + h if len(h) < 2 else h + _data += h - # constrain number of bytes in response - if (self.bytes > 0): # zero bytes means flexible response - _data = constrainHex(_data, self.bytes) + # constrain number of bytes in response + if (self.bytes > 0): # zero bytes means flexible response + _data = constrainHex(_data, self.bytes) # decoded value into the response object d = self.decode(_data) diff --git a/obd/commands.py b/obd/commands.py index 09cb0236..3c377937 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -29,6 +29,7 @@ # # ######################################################################## +from .protocols import ECU from .OBDCommand import OBDCommand from .decoders import * from .debug import debug @@ -44,107 +45,107 @@ # NOTE: commands MUST be in PID order, one command per PID (for fast lookup using __mode1__[pid]) __mode1__ = [ - # sensor name description mode cmd bytes decoder - OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "01", "00", 4, pid , True), # the first PID getter is assumed to be supported - OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01", 4, status ), - OBDCommand("FREEZE_DTC" , "Freeze DTC" , "01", "02", 2, noop ), - OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03", 2, fuel_status ), - OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , "01", "04", 1, percent ), - OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05", 1, temp ), - OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06", 1, percent_centered ), - OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07", 1, percent_centered ), - OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08", 1, percent_centered ), - OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09", 1, percent_centered ), - OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A", 1, fuel_pressure ), - OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "01", "0B", 1, pressure ), - OBDCommand("RPM" , "Engine RPM" , "01", "0C", 2, rpm ), - OBDCommand("SPEED" , "Vehicle Speed" , "01", "0D", 1, speed ), - OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E", 1, timing_advance ), - OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F", 1, temp ), - OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10", 2, maf ), - OBDCommand("THROTTLE_POS" , "Throttle Position" , "01", "11", 1, percent ), - OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12", 1, air_status ), - OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13", 1, noop ), - OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , "01", "14", 2, sensor_voltage ), - OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , "01", "15", 2, sensor_voltage ), - OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , "01", "16", 2, sensor_voltage ), - OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , "01", "17", 2, sensor_voltage ), - OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , "01", "18", 2, sensor_voltage ), - OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , "01", "19", 2, sensor_voltage ), - OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , "01", "1A", 2, sensor_voltage ), - OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , "01", "1B", 2, sensor_voltage ), - OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "01", "1C", 1, obd_compliance ), - OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D", 1, noop ), - OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E", 1, noop ), - OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F", 2, seconds ), - - # sensor name description mode cmd bytes decoder - OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "01", "20", 4, pid ), - OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , "01", "21", 2, distance ), - OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , "01", "22", 2, fuel_pres_vac ), - OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , "01", "23", 2, fuel_pres_direct ), - OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , "01", "24", 4, sensor_voltage_big ), - OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , "01", "25", 4, sensor_voltage_big ), - OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , "01", "26", 4, sensor_voltage_big ), - OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , "01", "27", 4, sensor_voltage_big ), - OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , "01", "28", 4, sensor_voltage_big ), - OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , "01", "29", 4, sensor_voltage_big ), - OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , "01", "2A", 4, sensor_voltage_big ), - OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , "01", "2B", 4, sensor_voltage_big ), - OBDCommand("COMMANDED_EGR" , "Commanded EGR" , "01", "2C", 1, percent ), - OBDCommand("EGR_ERROR" , "EGR Error" , "01", "2D", 1, percent_centered ), - OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , "01", "2E", 1, percent ), - OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , "01", "2F", 1, percent ), - OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , "01", "30", 1, count ), - OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , "01", "31", 2, distance ), - OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , "01", "32", 2, evap_pressure ), - OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , "01", "33", 1, pressure ), - OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , "01", "34", 4, current_centered ), - OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , "01", "35", 4, current_centered ), - OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , "01", "36", 4, current_centered ), - OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , "01", "37", 4, current_centered ), - OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , "01", "38", 4, current_centered ), - OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , "01", "39", 4, current_centered ), - OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , "01", "3A", 4, current_centered ), - OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , "01", "3B", 4, current_centered ), - OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , "01", "3C", 2, catalyst_temp ), - OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , "01", "3D", 2, catalyst_temp ), - OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , "01", "3E", 2, catalyst_temp ), - OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , "01", "3F", 2, catalyst_temp ), - - # sensor name description mode cmd bytes decoder - OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "01", "40", 4, pid ), - OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "01", "41", 4, todo ), - OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "01", "42", 2, todo ), - OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "01", "43", 2, todo ), - OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "01", "44", 2, todo ), - OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , "01", "45", 1, percent ), - OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , "01", "46", 1, temp ), - OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , "01", "47", 1, percent ), - OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , "01", "48", 1, percent ), - OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , "01", "49", 1, percent ), - OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , "01", "4A", 1, percent ), - OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , "01", "4B", 1, percent ), - OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , "01", "4C", 1, percent ), - OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , "01", "4D", 2, minutes ), - OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , "01", "4E", 2, minutes ), - OBDCommand("MAX_VALUES" , "Various Max values" , "01", "4F", 4, noop ), # todo: decode this - OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , "01", "50", 4, max_maf ), - OBDCommand("FUEL_TYPE" , "Fuel Type" , "01", "51", 1, fuel_type ), - OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , "01", "52", 1, percent ), - OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , "01", "53", 2, abs_evap_pressure ), - OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , "01", "54", 2, evap_pressure_alt ), - OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , "01", "55", 2, percent_centered ), # todo: decode seconds value for banks 3 and 4 - OBDCommand("LONG_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , "01", "56", 2, percent_centered ), - OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , "01", "57", 2, percent_centered ), - OBDCommand("LONG_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , "01", "58", 2, percent_centered ), - OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , "01", "59", 2, fuel_pres_direct ), - OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , "01", "5A", 1, percent ), - OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , "01", "5B", 1, percent ), - OBDCommand("OIL_TEMP" , "Engine oil temperature" , "01", "5C", 1, temp ), - OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , "01", "5D", 2, inject_timing ), - OBDCommand("FUEL_RATE" , "Engine fuel rate" , "01", "5E", 2, fuel_rate ), - OBDCommand("EMISSION_REQ" , "Designed emission requirements" , "01", "5F", 1, noop ), + # sensor name description mode cmd bytes decoder ECU + OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "01", "00", 4, pid, ECU.ALL , True), # the first PID getter is assumed to be supported + OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01", 4, status, ECU.ENGINE), + OBDCommand("FREEZE_DTC" , "Freeze DTC" , "01", "02", 2, noop, ECU.ENGINE), + OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03", 2, fuel_status, ECU.ENGINE), + OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , "01", "04", 1, percent, ECU.ENGINE), + OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05", 1, temp, ECU.ENGINE), + OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06", 1, percent_centered, ECU.ENGINE), + OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07", 1, percent_centered, ECU.ENGINE), + OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08", 1, percent_centered, ECU.ENGINE), + OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09", 1, percent_centered, ECU.ENGINE), + OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A", 1, fuel_pressure, ECU.ENGINE), + OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "01", "0B", 1, pressure, ECU.ENGINE), + OBDCommand("RPM" , "Engine RPM" , "01", "0C", 2, rpm, ECU.ENGINE), + OBDCommand("SPEED" , "Vehicle Speed" , "01", "0D", 1, speed, ECU.ENGINE), + OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E", 1, timing_advance, ECU.ENGINE), + OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F", 1, temp, ECU.ENGINE), + OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10", 2, maf, ECU.ENGINE), + OBDCommand("THROTTLE_POS" , "Throttle Position" , "01", "11", 1, percent, ECU.ENGINE), + OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12", 1, air_status, ECU.ENGINE), + OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13", 1, noop, ECU.ENGINE), + OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , "01", "14", 2, sensor_voltage, ECU.ENGINE), + OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , "01", "15", 2, sensor_voltage, ECU.ENGINE), + OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , "01", "16", 2, sensor_voltage, ECU.ENGINE), + OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , "01", "17", 2, sensor_voltage, ECU.ENGINE), + OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , "01", "18", 2, sensor_voltage, ECU.ENGINE), + OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , "01", "19", 2, sensor_voltage, ECU.ENGINE), + OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , "01", "1A", 2, sensor_voltage, ECU.ENGINE), + OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , "01", "1B", 2, sensor_voltage, ECU.ENGINE), + OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "01", "1C", 1, obd_compliance, ECU.ENGINE), + OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D", 1, noop, ECU.ENGINE), + OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E", 1, noop, ECU.ENGINE), + OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F", 2, seconds, ECU.ENGINE), + + # sensor name description mode cmd bytes decoder ECU + OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "01", "20", 4, pid, ECU.ALL ), + OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , "01", "21", 2, distance, ECU.ENGINE), + OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , "01", "22", 2, fuel_pres_vac, ECU.ENGINE), + OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , "01", "23", 2, fuel_pres_direct, ECU.ENGINE), + OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , "01", "24", 4, sensor_voltage_big, ECU.ENGINE), + OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , "01", "25", 4, sensor_voltage_big, ECU.ENGINE), + OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , "01", "26", 4, sensor_voltage_big, ECU.ENGINE), + OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , "01", "27", 4, sensor_voltage_big, ECU.ENGINE), + OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , "01", "28", 4, sensor_voltage_big, ECU.ENGINE), + OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , "01", "29", 4, sensor_voltage_big, ECU.ENGINE), + OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , "01", "2A", 4, sensor_voltage_big, ECU.ENGINE), + OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , "01", "2B", 4, sensor_voltage_big, ECU.ENGINE), + OBDCommand("COMMANDED_EGR" , "Commanded EGR" , "01", "2C", 1, percent, ECU.ENGINE), + OBDCommand("EGR_ERROR" , "EGR Error" , "01", "2D", 1, percent_centered, ECU.ENGINE), + OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , "01", "2E", 1, percent, ECU.ENGINE), + OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , "01", "2F", 1, percent, ECU.ENGINE), + OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , "01", "30", 1, count, ECU.ENGINE), + OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , "01", "31", 2, distance, ECU.ENGINE), + OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , "01", "32", 2, evap_pressure, ECU.ENGINE), + OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , "01", "33", 1, pressure, ECU.ENGINE), + OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , "01", "34", 4, current_centered, ECU.ENGINE), + OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , "01", "35", 4, current_centered, ECU.ENGINE), + OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , "01", "36", 4, current_centered, ECU.ENGINE), + OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , "01", "37", 4, current_centered, ECU.ENGINE), + OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , "01", "38", 4, current_centered, ECU.ENGINE), + OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , "01", "39", 4, current_centered, ECU.ENGINE), + OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , "01", "3A", 4, current_centered, ECU.ENGINE), + OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , "01", "3B", 4, current_centered, ECU.ENGINE), + OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , "01", "3C", 2, catalyst_temp, ECU.ENGINE), + OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , "01", "3D", 2, catalyst_temp, ECU.ENGINE), + OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , "01", "3E", 2, catalyst_temp, ECU.ENGINE), + OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , "01", "3F", 2, catalyst_temp, ECU.ENGINE), + + # sensor name description mode cmd bytes decoder ECU + OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "01", "40", 4, pid, ECU.ALL ), + OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "01", "41", 4, todo, ECU.ENGINE), + OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "01", "42", 2, todo, ECU.ENGINE), + OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "01", "43", 2, todo, ECU.ENGINE), + OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "01", "44", 2, todo, ECU.ENGINE), + OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , "01", "45", 1, percent, ECU.ENGINE), + OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , "01", "46", 1, temp, ECU.ENGINE), + OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , "01", "47", 1, percent, ECU.ENGINE), + OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , "01", "48", 1, percent, ECU.ENGINE), + OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , "01", "49", 1, percent, ECU.ENGINE), + OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , "01", "4A", 1, percent, ECU.ENGINE), + OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , "01", "4B", 1, percent, ECU.ENGINE), + OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , "01", "4C", 1, percent, ECU.ENGINE), + OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , "01", "4D", 2, minutes, ECU.ENGINE), + OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , "01", "4E", 2, minutes, ECU.ENGINE), + OBDCommand("MAX_VALUES" , "Various Max values" , "01", "4F", 4, noop, ECU.ENGINE), # todo: decode this + OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , "01", "50", 4, max_maf, ECU.ENGINE), + OBDCommand("FUEL_TYPE" , "Fuel Type" , "01", "51", 1, fuel_type, ECU.ENGINE), + OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , "01", "52", 1, percent, ECU.ENGINE), + OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , "01", "53", 2, abs_evap_pressure, ECU.ENGINE), + OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , "01", "54", 2, evap_pressure_alt, ECU.ENGINE), + OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , "01", "55", 2, percent_centered, ECU.ENGINE), # todo: decode seconds value for banks 3 and 4 + OBDCommand("LONG_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , "01", "56", 2, percent_centered, ECU.ENGINE), + OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , "01", "57", 2, percent_centered, ECU.ENGINE), + OBDCommand("LONG_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , "01", "58", 2, percent_centered, ECU.ENGINE), + OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , "01", "59", 2, fuel_pres_direct, ECU.ENGINE), + OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , "01", "5A", 1, percent, ECU.ENGINE), + OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , "01", "5B", 1, percent, ECU.ENGINE), + OBDCommand("OIL_TEMP" , "Engine oil temperature" , "01", "5C", 1, temp, ECU.ENGINE), + OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , "01", "5D", 2, inject_timing, ECU.ENGINE), + OBDCommand("FUEL_RATE" , "Engine fuel rate" , "01", "5E", 2, fuel_rate, ECU.ENGINE), + OBDCommand("EMISSION_REQ" , "Designed emission requirements" , "01", "5F", 1, noop, ECU.ENGINE), ] @@ -159,18 +160,18 @@ __mode3__ = [ - # sensor name description mode cmd bytes decoder - OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 0, dtc , True), + # sensor name description mode cmd bytes decoder ECU + OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 0, dtc, ECU.ALL, True), ] __mode4__ = [ - # sensor name description mode cmd bytes decoder - OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", "" , 0, noop , True), + # sensor name description mode cmd bytes decoder ECU + OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", "" , 0, noop, ECU.ALL, True), ] __mode7__ = [ - # sensor name description mode cmd bytes decoder - OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", "" , 0, dtc , True), + # sensor name description mode cmd bytes decoder ECU + OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", "" , 0, dtc, ECU.ALL, True), ] diff --git a/obd/elm327.py b/obd/elm327.py index ef19af3f..31ee4f17 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -71,7 +71,7 @@ def __init__(self, portname, baudrate): self.__status = SerialStatus.NOT_CONNECTED self.__port = None - self.__protocol = UnknownProtocol + self.__protocol = UnknownProtocol([]) self.__primary_ecu = None # message.tx_id @@ -167,16 +167,7 @@ def load_protocol(self): return False # instantiate the correct protocol handler - self.__protocol = self._SUPPORTED_PROTOCOLS[p]() - - # Now that a protocol has been selected, we can figure out - # which ECU is the primary. - - m = self.__protocol(r0100) - self.__primary_ecu = find_primary_ecu(m) - if self.__primary_ecu is None: - debug("Failed to choose primary ECU", True) - return False + self.__protocol = self._SUPPORTED_PROTOCOLS[p](r0100) return True diff --git a/obd/obd.py b/obd/obd.py index d2f3ba33..58024227 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -192,9 +192,4 @@ def query(self, cmd, force=False): debug("No valid OBD Messages returned", True) return OBDResponse() - # select the first message with the ECU ID we're looking for - for message in messages: - if message.tx_id == self.__primary_ecu: - return message - return cmd(messages) # compute a response object diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index c076a169..cf8f1b14 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -43,7 +43,7 @@ class ECU: """ constant flags used for marking and filtering messages """ - ANY = 0b11111111 # used by OBDCommands to accept messages from any ECU + ALL = 0b11111111 # used by OBDCommands to accept messages from any ECU UNKNOWN = 0b00000000 ENGINE = 0b00000001 # each ECU gets its own bit for ease of making OR filters diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 738c5bc1..f4cfa99a 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -43,8 +43,8 @@ class CANProtocol(Protocol): def __init__(self, lines_0100, id_bits): - Protocol.__init__(self, lines_0100) self.id_bits = id_bits + Protocol.__init__(self, lines_0100) def parse_frame(self, frame): diff --git a/obd/protocols/protocol_unknown.py b/obd/protocols/protocol_unknown.py index faa4836c..af9e1204 100644 --- a/obd/protocols/protocol_unknown.py +++ b/obd/protocols/protocol_unknown.py @@ -41,11 +41,8 @@ class UnknownProtocol(Protocol): been made, but the car hasn't responded. """ - def __init__(self): - Protocol.__init__(self) + def parse_frame(self, frame): + return True # pass everything - def parse_frame(self, raw): - return False - - def parse_message(self, frames, tx_id): - return False + def parse_message(self, message): + return True # pass everything From 898d77dd26791d2e19a087c77bc8737d448f96af Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 2 Jun 2015 13:51:57 -0400 Subject: [PATCH 241/569] fixed tests, added ECU_Map assert, simplified many OBD and ELM327 methods --- obd/OBDCommand.py | 6 +-- obd/__init__.py | 5 +- obd/elm327.py | 39 +++++++-------- obd/obd.py | 93 ++++++++++++++++++++--------------- obd/protocols/protocol.py | 24 ++++++--- obd/protocols/protocol_can.py | 2 + tests/test_OBDCommand.py | 33 +++++++------ 7 files changed, 112 insertions(+), 90 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 77141f5a..89fdb683 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -83,9 +83,9 @@ def __call__(self, messages): h = "0" + h if len(h) < 2 else h _data += h - # constrain number of bytes in response - if (self.bytes > 0): # zero bytes means flexible response - _data = constrainHex(_data, self.bytes) + # constrain number of bytes in response + if (self.bytes > 0): # zero bytes means flexible response + _data = constrainHex(_data, self.bytes) # decoded value into the response object d = self.decode(_data) diff --git a/obd/__init__.py b/obd/__init__.py index 6e43fd23..97bb579f 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -38,9 +38,10 @@ from .__version__ import __version__ from .obd import OBD +from .async import Async +from .commands import commands from .OBDCommand import OBDCommand +from .protocols import ECU from .OBDResponse import Unit -from .commands import commands from .utils import scanSerial, SerialStatus from .debug import debug -from .async import Async diff --git a/obd/elm327.py b/obd/elm327.py index 31ee4f17..1915f1ec 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -45,7 +45,7 @@ class ELM327: the following functions become available: send_and_parse() - get_port_name() + port_name() status() close() """ @@ -72,8 +72,6 @@ def __init__(self, portname, baudrate): self.__status = SerialStatus.NOT_CONNECTED self.__port = None self.__protocol = UnknownProtocol([]) - self.__primary_ecu = None # message.tx_id - # ------------- open port ------------- @@ -85,7 +83,7 @@ def __init__(self, portname, baudrate): stopbits = 1, \ bytesize = 8, \ timeout = 3) # seconds - debug("Serial port successfully opened on " + self.get_port_name()) + debug("Serial port successfully opened on " + self.port_name) except serial.SerialException as e: self.__error(e) @@ -95,7 +93,6 @@ def __init__(self, portname, baudrate): return - # ---------------------------- ATZ (reset) ---------------------------- try: self.__send("ATZ", delay=1) # wait 1 second for ELM to initialize @@ -129,7 +126,6 @@ def __init__(self, portname, baudrate): return - # try to communicate with the car, and load the correct protocol parser if self.load_protocol(): self.__status = SerialStatus.CAR_CONNECTED @@ -137,7 +133,6 @@ def __init__(self, portname, baudrate): self.__status = SerialStatus.ELM_CONNECTED - # ------------------------------- done ------------------------------- debug("Connection successful") @@ -184,19 +179,19 @@ def __isok(self, lines, expectEcho=False): def __error(self, msg=None): """ handles fatal failures, print debug info and closes serial """ - debug("Connection Error:", True) + self.close() + debug("Connection Error:", True) if msg is not None: debug(' ' + str(msg), True) - if self.__port is not None: - self.__port.close() - - self.__status = SerialStatus.NOT_CONNECTED - - def get_port_name(self): - return self.__port.portstr if (self.__port is not None) else "No Port" + @property + def port_name(self): + if self.__port is not None: + return self.__port.portstr + else: + return "No Port" @property @@ -206,17 +201,17 @@ def status(self): def close(self): """ - Resets the device, and clears all attributes to unconnected state + Resets the device, and sets all + attributes to unconnected states. """ - if self.__status >= SerialStatus.ELM_CONNECTED: + self.__status = SerialStatus.NOT_CONNECTED + self.__protocol = None + + if self.__port is not None: self.__write("ATZ") self.__port.close() - - self.__status = SerialStatus.NOT_CONNECTED - self.__port = None - self.__protocol = None - self.__primary_ecu = None + self.__port = None def send_and_parse(self, cmd): diff --git a/obd/obd.py b/obd/obd.py index 58024227..f233d28b 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -42,7 +42,8 @@ class OBD(object): """ - Class representing an OBD-II connection with it's assorted commands/sensors + Class representing an OBD-II connection + with it's assorted commands/sensors. """ def __init__(self, portstr=None, baudrate=38400): @@ -51,13 +52,13 @@ def __init__(self, portstr=None, baudrate=38400): debug("========================== python-OBD (v%s) ==========================" % __version__) self.__connect(portstr, baudrate) # initialize by connecting and loading sensors + self.__load_commands() # try to load the car's supported commands debug("=========================================================================") def __connect(self, portstr, baudrate): """ Attempts to instantiate an ELM327 connection object. - Upon success, __load_commands() is called """ if portstr is None: @@ -70,47 +71,15 @@ def __connect(self, portstr, baudrate): self.port = ELM327(port, baudrate) if self.port.status >= SerialStatus.ELM_CONNECTED: - # success! stop searching for serial - break + break # success! stop searching for serial else: debug("Explicit port defined") self.port = ELM327(portstr, baudrate) # if a connection was made, query for commands - if self.port.status == SerialStatus.CAR_CONNECTED: - self.__load_commands() - else: + if self.port.status == SerialStatus.NOT_CONNECTED: debug("Failed to connect") - - - def close(self): - """ Closes the connection """ - if self.status != SerialStatus.NOT_CONNECTED: - debug("Closing connection") - self.port.close() self.port = None - self.supported_commands = [] - - - @property - def status(self): - if self.port is None: - return SerialStatus.NOT_CONNECTED - else: - return self.port.status - - - def is_connected(self): - """ Returns a boolean for whether a successful serial connection was made """ - return self.status == SerialStatus.CAR_CONNECTED - - - def get_port_name(self): - """ Returns the name of the currently connected port """ - if self.status != SerialStatus.NOT_CONNECTED: - return self.port.get_port_name() - else: - return "Not connected to any port" def __load_commands(self): @@ -119,12 +88,12 @@ def __load_commands(self): and compiles a list of command objects. """ - debug("querying for supported PIDs (commands)...") - - self.supported_commands = [] + if self.status != SerialStatus.CAR_CONNECTED: + debug("Cannot load commands: No connection to car", True) + return + debug("querying for supported PIDs (commands)...") pid_getters = commands.pid_getters() - for get in pid_getters: # PID listing commands should sequentialy become supported # Mode 1 PID 0 is assumed to always be supported @@ -156,6 +125,45 @@ def __load_commands(self): debug("finished querying with %d commands supported" % len(self.supported_commands)) + def close(self): + """ + Closes the connection, and clear supported_commands + """ + + self.supported_commands = [] + + if self.port is not None: + debug("Closing connection") + self.port.close() + self.port = None + + + @property + def status(self): + if self.port is None: + return SerialStatus.NOT_CONNECTED + else: + return self.port.status + + + def is_connected(self): + """ + Returns a boolean for whether a connection with the car was made. + + Note: this function returns False when: + obd.status = SerialStatus.ELM_CONNECTED + """ + return self.status == SerialStatus.CAR_CONNECTED + + + def get_port_name(self): + """ Returns the name of the currently connected port """ + if self.port is not None: + return self.port.port_name + else: + return "Not connected to any port" + + def print_commands(self): """ Utility function meant for working in interactive mode. @@ -166,7 +174,10 @@ def print_commands(self): def supports(self, cmd): - """ Returns a boolean for whether the car supports the given command """ + """ + Returns a boolean for whether the given command + is supported by the car AND this library + """ return commands.has_command(cmd) and cmd.supported diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index cf8f1b14..120751f5 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -44,10 +44,11 @@ class ECU: """ constant flags used for marking and filtering messages """ ALL = 0b11111111 # used by OBDCommands to accept messages from any ECU - UNKNOWN = 0b00000000 - ENGINE = 0b00000001 # each ECU gets its own bit for ease of making OR filters - # TRANSMISSION = 0b00000010 + # each ECU gets its own bit for ease of making OR filters + UNKNOWN = 0b00000001 # unknowns get their own bit, since they need to be accepted by the ALL filter + ENGINE = 0b00000010 + TRANSMISSION = 0b00000100 class ECU_Map: @@ -58,7 +59,8 @@ def __init__(self, init_map): self.backward_map = {} # ECU ID ---> tx_id # the backwards map is simply used to check for ECU ID collisions - # since it shouldn't be possible to have two tx_id's represent the engine + # since, for example, it shouldn't be possible to have two + # tx_id's represent the engine. # construct the backwards map for key in self.forward_map: @@ -66,7 +68,14 @@ def __init__(self, init_map): self.backward_map[value] = key def set(self, tx_id, ecu_id): - """ maps a tx_id to an ECU ID, and remove any old mappings to that ECU ID """ + """ + maps a tx_id to an ECU ID, and removes + any old mappings to that ECU ID + """ + + # nevery store ECU.UNKNOWNs + # this is the only case where multiple tx_ids resolve to the same ECU ID + assert ecu_id != ECU.UNKNOWN # check the backwards map to see if this ECU ID was already registered if ecu_id in self.backward_map: @@ -87,7 +96,10 @@ def resolve(self, tx_id): return ECU.UNKNOWN def lookup(self, ecu_id): - """ converts an ECU ID constant into a tx_id (mostly for testing) """ + """ + converts an ECU ID constant into a tx_id + (mostly for testing) + """ if ecu_id in self.backward_map: return self.backward_map[ecu_id] else: diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index f4cfa99a..f95138fa 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -43,6 +43,8 @@ class CANProtocol(Protocol): def __init__(self, lines_0100, id_bits): + # this needs to be set FIRST, since the base + # Protocol __init__ uses the parsing system. self.id_bits = id_bits Protocol.__init__(self, lines_0100) diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index e049e2a7..65bb6f6e 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -2,31 +2,31 @@ from obd.commands import OBDCommand from obd.decoders import noop from obd.protocols import * -from obd.protocols.protocol import Message def test_constructor(): # name description mode cmd bytes decoder - cmd = OBDCommand("Test", "example OBD command", "01", "23", 2, noop) + cmd = OBDCommand("Test", "example OBD command", "01", "23", 2, noop, ECU.ENGINE) assert cmd.name == "Test" assert cmd.desc == "example OBD command" assert cmd.mode == "01" assert cmd.pid == "23" assert cmd.bytes == 2 assert cmd.decode == noop + assert cmd.ecu == ECU.ENGINE assert cmd.supported == False assert cmd.get_command() == "0123" assert cmd.get_mode_int() == 1 assert cmd.get_pid_int() == 35 - cmd = OBDCommand("Test", "example OBD command", "01", "23", 2, noop, True) + cmd = OBDCommand("Test", "example OBD command", "01", "23", 2, noop, ECU.ENGINE, True) assert cmd.supported == True def test_clone(): # name description mode cmd bytes decoder - cmd = OBDCommand("", "", "01", "23", 2, noop) + cmd = OBDCommand("", "", "01", "23", 2, noop, ECU.ENGINE) other = cmd.clone() assert cmd.name == other.name @@ -35,45 +35,46 @@ def test_clone(): assert cmd.pid == other.pid assert cmd.bytes == other.bytes assert cmd.decode == other.decode + assert cmd.ecu == other.ecu assert cmd.supported == cmd.supported def test_call(): p = SAE_J1850_PWM([]) - m = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) # parse valid data into response object + messages = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) # parse valid data into response object # valid response size - cmd = OBDCommand("", "", "01", "23", 4, noop) - r = cmd(m[0]) + cmd = OBDCommand("", "", "01", "23", 4, noop, ECU.ENGINE) + r = cmd(messages) assert r.value == "BE1FB811" # response too short (pad) - cmd = OBDCommand("", "", "01", "23", 5, noop) - r = cmd(m[0]) + cmd = OBDCommand("", "", "01", "23", 5, noop, ECU.ENGINE) + r = cmd(messages) assert r.value == "BE1FB81100" # response too long (clip) - cmd = OBDCommand("", "", "01", "23", 3, noop) - r = cmd(m[0]) + cmd = OBDCommand("", "", "01", "23", 3, noop, ECU.ENGINE) + r = cmd(messages) assert r.value == "BE1FB8" def test_get_command(): - cmd = OBDCommand("", "", "01", "23", 4, noop) + cmd = OBDCommand("", "", "01", "23", 4, noop, ECU.ENGINE) assert cmd.get_command() == "0123" # simple concat of mode and PID def test_get_mode_int(): - cmd = OBDCommand("", "", "01", "23", 4, noop) + cmd = OBDCommand("", "", "01", "23", 4, noop, ECU.ENGINE) assert cmd.get_mode_int() == 0x01 - cmd = OBDCommand("", "", "", "23", 4, noop) + cmd = OBDCommand("", "", "", "23", 4, noop, ECU.ENGINE) assert cmd.get_mode_int() == 0 def test_get_pid_int(): - cmd = OBDCommand("", "", "01", "23", 4, noop) + cmd = OBDCommand("", "", "01", "23", 4, noop, ECU.ENGINE) assert cmd.get_pid_int() == 0x23 - cmd = OBDCommand("", "", "01", "", 4, noop) + cmd = OBDCommand("", "", "01", "", 4, noop, ECU.ENGINE) assert cmd.get_pid_int() == 0 From c3693bff18307a97a533fa2090872044eeaa9138 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 2 Jun 2015 14:07:54 -0400 Subject: [PATCH 242/569] changed from seperate Mode and PID attributes, to a single Command attribute --- obd/OBDCommand.py | 45 +++++----- obd/commands.py | 214 +++++++++++++++++++++++----------------------- obd/obd.py | 6 +- 3 files changed, 134 insertions(+), 131 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 89fdb683..2c2ecbbe 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -35,33 +35,36 @@ class OBDCommand(): - def __init__(self, name, desc, mode, pid, returnBytes, decoder, ecu, supported=False): - self.name = name - self.desc = desc - self.mode = mode - self.pid = pid - self.bytes = returnBytes # number of bytes expected in return - self.decode = decoder - self.ecu = ecu - self.supported = supported + def __init__(self, name, desc, command, returnBytes, decoder, ecu, supported=False): + self.name = name # human readable name (also used as key in commands dict) + self.desc = desc # human readable description + self.command = command # command string + self.bytes = returnBytes # number of bytes expected in return + self.decode = decoder # decoding function + self.ecu = ecu # ECU ID from which this command expects messages from + self.supported = supported # bool for support def clone(self): return OBDCommand(self.name, self.desc, - self.mode, - self.pid, + self.command, self.bytes, self.decode, self.ecu) - def get_command(self): - return self.mode + self.pid # the actual command transmitted to the port - - def get_mode_int(self): - return unhex(self.mode) + @property + def mode_int(self): + if len(self.command) >= 2: + return unhex(self.command[:2]) + else: + return 0 - def get_pid_int(self): - return unhex(self.pid) + @property + def pid_int(self): + if len(self.command) > 2: + return unhex(self.command[2:]) + else: + return 0 def __call__(self, messages): @@ -95,14 +98,14 @@ def __call__(self, messages): return r def __str__(self): - return "%s%s: %s" % (self.mode, self.pid, self.desc) + return "%s: %s" % (self.command, self.desc) def __hash__(self): # needed for using commands as keys in a dict (see async.py) - return hash((self.mode, self.pid)) + return hash(self.command) def __eq__(self, other): if isinstance(other, OBDCommand): - return (self.mode, self.pid) == (other.mode, other.pid) + return (self.command == other.command) else: return False diff --git a/obd/commands.py b/obd/commands.py index 3c377937..a18ba7c5 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -45,107 +45,107 @@ # NOTE: commands MUST be in PID order, one command per PID (for fast lookup using __mode1__[pid]) __mode1__ = [ - # sensor name description mode cmd bytes decoder ECU - OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "01", "00", 4, pid, ECU.ALL , True), # the first PID getter is assumed to be supported - OBDCommand("STATUS" , "Status since DTCs cleared" , "01", "01", 4, status, ECU.ENGINE), - OBDCommand("FREEZE_DTC" , "Freeze DTC" , "01", "02", 2, noop, ECU.ENGINE), - OBDCommand("FUEL_STATUS" , "Fuel System Status" , "01", "03", 2, fuel_status, ECU.ENGINE), - OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , "01", "04", 1, percent, ECU.ENGINE), - OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "01", "05", 1, temp, ECU.ENGINE), - OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "01", "06", 1, percent_centered, ECU.ENGINE), - OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "01", "07", 1, percent_centered, ECU.ENGINE), - OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "01", "08", 1, percent_centered, ECU.ENGINE), - OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "01", "09", 1, percent_centered, ECU.ENGINE), - OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "01", "0A", 1, fuel_pressure, ECU.ENGINE), - OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "01", "0B", 1, pressure, ECU.ENGINE), - OBDCommand("RPM" , "Engine RPM" , "01", "0C", 2, rpm, ECU.ENGINE), - OBDCommand("SPEED" , "Vehicle Speed" , "01", "0D", 1, speed, ECU.ENGINE), - OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "01", "0E", 1, timing_advance, ECU.ENGINE), - OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "01", "0F", 1, temp, ECU.ENGINE), - OBDCommand("MAF" , "Air Flow Rate (MAF)" , "01", "10", 2, maf, ECU.ENGINE), - OBDCommand("THROTTLE_POS" , "Throttle Position" , "01", "11", 1, percent, ECU.ENGINE), - OBDCommand("AIR_STATUS" , "Secondary Air Status" , "01", "12", 1, air_status, ECU.ENGINE), - OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "01", "13", 1, noop, ECU.ENGINE), - OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , "01", "14", 2, sensor_voltage, ECU.ENGINE), - OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , "01", "15", 2, sensor_voltage, ECU.ENGINE), - OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , "01", "16", 2, sensor_voltage, ECU.ENGINE), - OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , "01", "17", 2, sensor_voltage, ECU.ENGINE), - OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , "01", "18", 2, sensor_voltage, ECU.ENGINE), - OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , "01", "19", 2, sensor_voltage, ECU.ENGINE), - OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , "01", "1A", 2, sensor_voltage, ECU.ENGINE), - OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , "01", "1B", 2, sensor_voltage, ECU.ENGINE), - OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "01", "1C", 1, obd_compliance, ECU.ENGINE), - OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "01", "1D", 1, noop, ECU.ENGINE), - OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "01", "1E", 1, noop, ECU.ENGINE), - OBDCommand("RUN_TIME" , "Engine Run Time" , "01", "1F", 2, seconds, ECU.ENGINE), - - # sensor name description mode cmd bytes decoder ECU - OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "01", "20", 4, pid, ECU.ALL ), - OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , "01", "21", 2, distance, ECU.ENGINE), - OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , "01", "22", 2, fuel_pres_vac, ECU.ENGINE), - OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , "01", "23", 2, fuel_pres_direct, ECU.ENGINE), - OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , "01", "24", 4, sensor_voltage_big, ECU.ENGINE), - OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , "01", "25", 4, sensor_voltage_big, ECU.ENGINE), - OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , "01", "26", 4, sensor_voltage_big, ECU.ENGINE), - OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , "01", "27", 4, sensor_voltage_big, ECU.ENGINE), - OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , "01", "28", 4, sensor_voltage_big, ECU.ENGINE), - OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , "01", "29", 4, sensor_voltage_big, ECU.ENGINE), - OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , "01", "2A", 4, sensor_voltage_big, ECU.ENGINE), - OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , "01", "2B", 4, sensor_voltage_big, ECU.ENGINE), - OBDCommand("COMMANDED_EGR" , "Commanded EGR" , "01", "2C", 1, percent, ECU.ENGINE), - OBDCommand("EGR_ERROR" , "EGR Error" , "01", "2D", 1, percent_centered, ECU.ENGINE), - OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , "01", "2E", 1, percent, ECU.ENGINE), - OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , "01", "2F", 1, percent, ECU.ENGINE), - OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , "01", "30", 1, count, ECU.ENGINE), - OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , "01", "31", 2, distance, ECU.ENGINE), - OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , "01", "32", 2, evap_pressure, ECU.ENGINE), - OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , "01", "33", 1, pressure, ECU.ENGINE), - OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , "01", "34", 4, current_centered, ECU.ENGINE), - OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , "01", "35", 4, current_centered, ECU.ENGINE), - OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , "01", "36", 4, current_centered, ECU.ENGINE), - OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , "01", "37", 4, current_centered, ECU.ENGINE), - OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , "01", "38", 4, current_centered, ECU.ENGINE), - OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , "01", "39", 4, current_centered, ECU.ENGINE), - OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , "01", "3A", 4, current_centered, ECU.ENGINE), - OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , "01", "3B", 4, current_centered, ECU.ENGINE), - OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , "01", "3C", 2, catalyst_temp, ECU.ENGINE), - OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , "01", "3D", 2, catalyst_temp, ECU.ENGINE), - OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , "01", "3E", 2, catalyst_temp, ECU.ENGINE), - OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , "01", "3F", 2, catalyst_temp, ECU.ENGINE), - - # sensor name description mode cmd bytes decoder ECU - OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "01", "40", 4, pid, ECU.ALL ), - OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "01", "41", 4, todo, ECU.ENGINE), - OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "01", "42", 2, todo, ECU.ENGINE), - OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "01", "43", 2, todo, ECU.ENGINE), - OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "01", "44", 2, todo, ECU.ENGINE), - OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , "01", "45", 1, percent, ECU.ENGINE), - OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , "01", "46", 1, temp, ECU.ENGINE), - OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , "01", "47", 1, percent, ECU.ENGINE), - OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , "01", "48", 1, percent, ECU.ENGINE), - OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , "01", "49", 1, percent, ECU.ENGINE), - OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , "01", "4A", 1, percent, ECU.ENGINE), - OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , "01", "4B", 1, percent, ECU.ENGINE), - OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , "01", "4C", 1, percent, ECU.ENGINE), - OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , "01", "4D", 2, minutes, ECU.ENGINE), - OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , "01", "4E", 2, minutes, ECU.ENGINE), - OBDCommand("MAX_VALUES" , "Various Max values" , "01", "4F", 4, noop, ECU.ENGINE), # todo: decode this - OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , "01", "50", 4, max_maf, ECU.ENGINE), - OBDCommand("FUEL_TYPE" , "Fuel Type" , "01", "51", 1, fuel_type, ECU.ENGINE), - OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , "01", "52", 1, percent, ECU.ENGINE), - OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , "01", "53", 2, abs_evap_pressure, ECU.ENGINE), - OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , "01", "54", 2, evap_pressure_alt, ECU.ENGINE), - OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , "01", "55", 2, percent_centered, ECU.ENGINE), # todo: decode seconds value for banks 3 and 4 - OBDCommand("LONG_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , "01", "56", 2, percent_centered, ECU.ENGINE), - OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , "01", "57", 2, percent_centered, ECU.ENGINE), - OBDCommand("LONG_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , "01", "58", 2, percent_centered, ECU.ENGINE), - OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , "01", "59", 2, fuel_pres_direct, ECU.ENGINE), - OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , "01", "5A", 1, percent, ECU.ENGINE), - OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , "01", "5B", 1, percent, ECU.ENGINE), - OBDCommand("OIL_TEMP" , "Engine oil temperature" , "01", "5C", 1, temp, ECU.ENGINE), - OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , "01", "5D", 2, inject_timing, ECU.ENGINE), - OBDCommand("FUEL_RATE" , "Engine fuel rate" , "01", "5E", 2, fuel_rate, ECU.ENGINE), - OBDCommand("EMISSION_REQ" , "Designed emission requirements" , "01", "5F", 1, noop, ECU.ENGINE), + # sensor name description cmd bytes decoder ECU + OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "0100", 4, pid, ECU.ALL , True), # the first PID getter is assumed to be supported + OBDCommand("STATUS" , "Status since DTCs cleared" , "0101", 4, status, ECU.ENGINE), + OBDCommand("FREEZE_DTC" , "Freeze DTC" , "0102", 2, noop, ECU.ENGINE), + OBDCommand("FUEL_STATUS" , "Fuel System Status" , "0103", 2, fuel_status, ECU.ENGINE), + OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , "0104", 1, percent, ECU.ENGINE), + OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "0105", 1, temp, ECU.ENGINE), + OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "0106", 1, percent_centered, ECU.ENGINE), + OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "0107", 1, percent_centered, ECU.ENGINE), + OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "0108", 1, percent_centered, ECU.ENGINE), + OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "0109", 1, percent_centered, ECU.ENGINE), + OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "010A", 1, fuel_pressure, ECU.ENGINE), + OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "010B", 1, pressure, ECU.ENGINE), + OBDCommand("RPM" , "Engine RPM" , "010C", 2, rpm, ECU.ENGINE), + OBDCommand("SPEED" , "Vehicle Speed" , "010D", 1, speed, ECU.ENGINE), + OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "010E", 1, timing_advance, ECU.ENGINE), + OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "010F", 1, temp, ECU.ENGINE), + OBDCommand("MAF" , "Air Flow Rate (MAF)" , "0110", 2, maf, ECU.ENGINE), + OBDCommand("THROTTLE_POS" , "Throttle Position" , "0111", 1, percent, ECU.ENGINE), + OBDCommand("AIR_STATUS" , "Secondary Air Status" , "0112", 1, air_status, ECU.ENGINE), + OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "0113", 1, noop, ECU.ENGINE), + OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , "0114", 2, sensor_voltage, ECU.ENGINE), + OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , "0115", 2, sensor_voltage, ECU.ENGINE), + OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , "0116", 2, sensor_voltage, ECU.ENGINE), + OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , "0117", 2, sensor_voltage, ECU.ENGINE), + OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , "0118", 2, sensor_voltage, ECU.ENGINE), + OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , "0119", 2, sensor_voltage, ECU.ENGINE), + OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , "011A", 2, sensor_voltage, ECU.ENGINE), + OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , "011B", 2, sensor_voltage, ECU.ENGINE), + OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "011C", 1, obd_compliance, ECU.ENGINE), + OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "011D", 1, noop, ECU.ENGINE), + OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "011E", 1, noop, ECU.ENGINE), + OBDCommand("RUN_TIME" , "Engine Run Time" , "011F", 2, seconds, ECU.ENGINE), + + # sensor name description cmd bytes decoder ECU + OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "0120", 4, pid, ECU.ALL ), + OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , "0121", 2, distance, ECU.ENGINE), + OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , "0122", 2, fuel_pres_vac, ECU.ENGINE), + OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , "0123", 2, fuel_pres_direct, ECU.ENGINE), + OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , "0124", 4, sensor_voltage_big, ECU.ENGINE), + OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , "0125", 4, sensor_voltage_big, ECU.ENGINE), + OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , "0126", 4, sensor_voltage_big, ECU.ENGINE), + OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , "0127", 4, sensor_voltage_big, ECU.ENGINE), + OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , "0128", 4, sensor_voltage_big, ECU.ENGINE), + OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , "0129", 4, sensor_voltage_big, ECU.ENGINE), + OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , "012A", 4, sensor_voltage_big, ECU.ENGINE), + OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , "012B", 4, sensor_voltage_big, ECU.ENGINE), + OBDCommand("COMMANDED_EGR" , "Commanded EGR" , "012C", 1, percent, ECU.ENGINE), + OBDCommand("EGR_ERROR" , "EGR Error" , "012D", 1, percent_centered, ECU.ENGINE), + OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , "012E", 1, percent, ECU.ENGINE), + OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , "012F", 1, percent, ECU.ENGINE), + OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , "0130", 1, count, ECU.ENGINE), + OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , "0131", 2, distance, ECU.ENGINE), + OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , "0132", 2, evap_pressure, ECU.ENGINE), + OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , "0133", 1, pressure, ECU.ENGINE), + OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , "0134", 4, current_centered, ECU.ENGINE), + OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , "0135", 4, current_centered, ECU.ENGINE), + OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , "0136", 4, current_centered, ECU.ENGINE), + OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , "0137", 4, current_centered, ECU.ENGINE), + OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , "0138", 4, current_centered, ECU.ENGINE), + OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , "0139", 4, current_centered, ECU.ENGINE), + OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , "013A", 4, current_centered, ECU.ENGINE), + OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , "013B", 4, current_centered, ECU.ENGINE), + OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , "013C", 2, catalyst_temp, ECU.ENGINE), + OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , "013D", 2, catalyst_temp, ECU.ENGINE), + OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , "013E", 2, catalyst_temp, ECU.ENGINE), + OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , "013F", 2, catalyst_temp, ECU.ENGINE), + + # sensor name description cmd bytes decoder ECU + OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "0140", 4, pid, ECU.ALL ), + OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "0141", 4, todo, ECU.ENGINE), + OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "0142", 2, todo, ECU.ENGINE), + OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "0143", 2, todo, ECU.ENGINE), + OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "0144", 2, todo, ECU.ENGINE), + OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , "0145", 1, percent, ECU.ENGINE), + OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , "0146", 1, temp, ECU.ENGINE), + OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , "0147", 1, percent, ECU.ENGINE), + OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , "0148", 1, percent, ECU.ENGINE), + OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , "0149", 1, percent, ECU.ENGINE), + OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , "014A", 1, percent, ECU.ENGINE), + OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , "014B", 1, percent, ECU.ENGINE), + OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , "014C", 1, percent, ECU.ENGINE), + OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , "014D", 2, minutes, ECU.ENGINE), + OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , "014E", 2, minutes, ECU.ENGINE), + OBDCommand("MAX_VALUES" , "Various Max values" , "014F", 4, noop, ECU.ENGINE), # todo: decode this + OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , "0150", 4, max_maf, ECU.ENGINE), + OBDCommand("FUEL_TYPE" , "Fuel Type" , "0151", 1, fuel_type, ECU.ENGINE), + OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , "0152", 1, percent, ECU.ENGINE), + OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , "0153", 2, abs_evap_pressure, ECU.ENGINE), + OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , "0154", 2, evap_pressure_alt, ECU.ENGINE), + OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , "0155", 2, percent_centered, ECU.ENGINE), # todo: decode seconds value for banks 3 and 4 + OBDCommand("LONG_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , "0156", 2, percent_centered, ECU.ENGINE), + OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , "0157", 2, percent_centered, ECU.ENGINE), + OBDCommand("LONG_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , "0158", 2, percent_centered, ECU.ENGINE), + OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , "0159", 2, fuel_pres_direct, ECU.ENGINE), + OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , "015A", 1, percent, ECU.ENGINE), + OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , "015B", 1, percent, ECU.ENGINE), + OBDCommand("OIL_TEMP" , "Engine oil temperature" , "015C", 1, temp, ECU.ENGINE), + OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , "015D", 2, inject_timing, ECU.ENGINE), + OBDCommand("FUEL_RATE" , "Engine fuel rate" , "015E", 2, fuel_rate, ECU.ENGINE), + OBDCommand("EMISSION_REQ" , "Designed emission requirements" , "015F", 1, noop, ECU.ENGINE), ] @@ -160,18 +160,18 @@ __mode3__ = [ - # sensor name description mode cmd bytes decoder ECU - OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 0, dtc, ECU.ALL, True), + # sensor name description cmd bytes decoder ECU + OBDCommand("GET_DTC" , "Get DTCs" , "03", 0, dtc, ECU.ALL, True), ] __mode4__ = [ - # sensor name description mode cmd bytes decoder ECU - OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", "" , 0, noop, ECU.ALL, True), + # sensor name description cmd bytes decoder ECU + OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", 0, noop, ECU.ALL, True), ] __mode7__ = [ - # sensor name description mode cmd bytes decoder ECU - OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", "" , 0, dtc, ECU.ALL, True), + # sensor name description cmd bytes decoder ECU + OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", 0, dtc, ECU.ALL, True), ] diff --git a/obd/obd.py b/obd/obd.py index f233d28b..ee098fa7 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -111,8 +111,8 @@ def __load_commands(self): for i in range(len(supported)): if supported[i] == "1": - mode = get.get_mode_int() - pid = get.get_pid_int() + i + 1 + mode = get.mode_int + pid = get.pid_int + i + 1 if commands.has_pid(mode, pid): c = commands[mode][pid] @@ -197,7 +197,7 @@ def query(self, cmd, force=False): # send command and retrieve message debug("Sending command: %s" % str(cmd)) - messages = self.port.send_and_parse(cmd.get_command()) + messages = self.port.send_and_parse(cmd.command) if not messages: debug("No valid OBD Messages returned", True) From 0527cd6b47c38226c624c3eeae98eb79dbb44e11 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 2 Jun 2015 14:20:17 -0400 Subject: [PATCH 243/569] fixed tests and mode 02 command tables --- obd/commands.py | 2 +- tests/test_OBD.py | 2 +- tests/test_OBDCommand.py | 44 ++++++++++++++++------------------------ tests/test_commands.py | 14 +++++++------ 4 files changed, 28 insertions(+), 34 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index a18ba7c5..d6bc9d85 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -153,7 +153,7 @@ __mode2__ = [] for c in __mode1__: c = c.clone() - c.mode = "02" + c.command = "02" + c.command[2:] # change the mode: 0100 ---> 0200 c.name = "DTC_" + c.name c.desc = "DTC " + c.desc __mode2__.append(c) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index b94fb0dd..88e5b262 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -19,7 +19,7 @@ def test_query(): # we don't need an actual serial connection o = obd.OBD("/dev/null") # forge our own command, to control the output - cmd = OBDCommand("TEST", "Test command", "01", "23", 2, noop, False) + cmd = OBDCommand("TEST", "Test command", "0123", 2, noop, False) # forge IO from the car by overwriting the read/write functions diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index 65bb6f6e..6e3eacfb 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -6,33 +6,30 @@ def test_constructor(): # name description mode cmd bytes decoder - cmd = OBDCommand("Test", "example OBD command", "01", "23", 2, noop, ECU.ENGINE) + cmd = OBDCommand("Test", "example OBD command", "0123", 2, noop, ECU.ENGINE) assert cmd.name == "Test" assert cmd.desc == "example OBD command" - assert cmd.mode == "01" - assert cmd.pid == "23" + assert cmd.command == "0123" assert cmd.bytes == 2 assert cmd.decode == noop assert cmd.ecu == ECU.ENGINE assert cmd.supported == False - assert cmd.get_command() == "0123" - assert cmd.get_mode_int() == 1 - assert cmd.get_pid_int() == 35 + assert cmd.mode_int == 1 + assert cmd.pid_int == 35 - cmd = OBDCommand("Test", "example OBD command", "01", "23", 2, noop, ECU.ENGINE, True) + cmd = OBDCommand("Test", "example OBD command", "0123", 2, noop, ECU.ENGINE, True) assert cmd.supported == True def test_clone(): # name description mode cmd bytes decoder - cmd = OBDCommand("", "", "01", "23", 2, noop, ECU.ENGINE) + cmd = OBDCommand("", "", "0123", 2, noop, ECU.ENGINE) other = cmd.clone() assert cmd.name == other.name assert cmd.desc == other.desc - assert cmd.mode == other.mode - assert cmd.pid == other.pid + assert cmd.command == other.command assert cmd.bytes == other.bytes assert cmd.decode == other.decode assert cmd.ecu == other.ecu @@ -44,37 +41,32 @@ def test_call(): messages = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) # parse valid data into response object # valid response size - cmd = OBDCommand("", "", "01", "23", 4, noop, ECU.ENGINE) + cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) r = cmd(messages) assert r.value == "BE1FB811" # response too short (pad) - cmd = OBDCommand("", "", "01", "23", 5, noop, ECU.ENGINE) + cmd = OBDCommand("", "", "0123", 5, noop, ECU.ENGINE) r = cmd(messages) assert r.value == "BE1FB81100" # response too long (clip) - cmd = OBDCommand("", "", "01", "23", 3, noop, ECU.ENGINE) + cmd = OBDCommand("", "", "0123", 3, noop, ECU.ENGINE) r = cmd(messages) assert r.value == "BE1FB8" -def test_get_command(): - cmd = OBDCommand("", "", "01", "23", 4, noop, ECU.ENGINE) - assert cmd.get_command() == "0123" # simple concat of mode and PID - - def test_get_mode_int(): - cmd = OBDCommand("", "", "01", "23", 4, noop, ECU.ENGINE) - assert cmd.get_mode_int() == 0x01 + cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) + assert cmd.mode_int == 0x01 cmd = OBDCommand("", "", "", "23", 4, noop, ECU.ENGINE) - assert cmd.get_mode_int() == 0 + assert cmd.mode_int == 0 -def test_get_pid_int(): - cmd = OBDCommand("", "", "01", "23", 4, noop, ECU.ENGINE) - assert cmd.get_pid_int() == 0x23 +def test_pid_int(): + cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) + assert cmd.pid_int == 0x23 - cmd = OBDCommand("", "", "01", "", 4, noop, ECU.ENGINE) - assert cmd.get_pid_int() == 0 + cmd = OBDCommand("", "", "01", 4, noop, ECU.ENGINE) + assert cmd.pid_int == 0 diff --git a/tests/test_commands.py b/tests/test_commands.py index e1cad593..b621d00a 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -7,9 +7,11 @@ def test_list_integrity(): for mode, cmds in enumerate(obd.commands.modes): for pid, cmd in enumerate(cmds): + assert cmd.command != "", "The Command's command string must not be null" + # make sure the command tables are in mode & PID order - assert mode == cmd.get_mode_int(), "Command is in the wrong mode list: %s" % cmd.name - assert pid == cmd.get_pid_int(), "The index in the list must also be the PID: %s" % cmd.name + assert mode == cmd.mode_int, "Command is in the wrong mode list: %s" % cmd.name + assert pid == cmd.pid_int, "The index in the list must also be the PID: %s" % cmd.name # make sure all the fields are set assert cmd.name != "", "Command names must not be null" @@ -38,8 +40,8 @@ def test_getitem(): for cmd in cmds: # by [mode][pid] - mode = cmd.get_mode_int() - pid = cmd.get_pid_int() + mode = cmd.mode_int + pid = cmd.pid_int assert cmd == obd.commands[mode][pid], "mode %d, PID %d could not be accessed through __getitem__" % (mode, pid) # by [name] @@ -55,8 +57,8 @@ def test_contains(): assert obd.commands.has_command(cmd) # by (mode, pid) - mode = cmd.get_mode_int() - pid = cmd.get_pid_int() + mode = cmd.mode_int + pid = cmd.pid_int assert obd.commands.has_pid(mode, pid) # by (name) From 90da3fad23ca2cc86a3443ee6aa0cd7beaef0ce7 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 2 Jun 2015 14:27:54 -0400 Subject: [PATCH 244/569] comment tweak, omit the word SENSOR --- obd/commands.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index d6bc9d85..76c3f2bf 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -41,11 +41,11 @@ Define command tables ''' -# NOTE: the SENSOR NAME field will be used as the dict key for that sensor +# NOTE: the NAME field will be used as the dict key for that sensor # NOTE: commands MUST be in PID order, one command per PID (for fast lookup using __mode1__[pid]) __mode1__ = [ - # sensor name description cmd bytes decoder ECU + # name description cmd bytes decoder ECU OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "0100", 4, pid, ECU.ALL , True), # the first PID getter is assumed to be supported OBDCommand("STATUS" , "Status since DTCs cleared" , "0101", 4, status, ECU.ENGINE), OBDCommand("FREEZE_DTC" , "Freeze DTC" , "0102", 2, noop, ECU.ENGINE), @@ -79,7 +79,7 @@ OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "011E", 1, noop, ECU.ENGINE), OBDCommand("RUN_TIME" , "Engine Run Time" , "011F", 2, seconds, ECU.ENGINE), - # sensor name description cmd bytes decoder ECU + # name description cmd bytes decoder ECU OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "0120", 4, pid, ECU.ALL ), OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , "0121", 2, distance, ECU.ENGINE), OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , "0122", 2, fuel_pres_vac, ECU.ENGINE), @@ -113,7 +113,7 @@ OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , "013E", 2, catalyst_temp, ECU.ENGINE), OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , "013F", 2, catalyst_temp, ECU.ENGINE), - # sensor name description cmd bytes decoder ECU + # name description cmd bytes decoder ECU OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "0140", 4, pid, ECU.ALL ), OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "0141", 4, todo, ECU.ENGINE), OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "0142", 2, todo, ECU.ENGINE), @@ -160,24 +160,24 @@ __mode3__ = [ - # sensor name description cmd bytes decoder ECU + # name description cmd bytes decoder ECU OBDCommand("GET_DTC" , "Get DTCs" , "03", 0, dtc, ECU.ALL, True), ] __mode4__ = [ - # sensor name description cmd bytes decoder ECU + # name description cmd bytes decoder ECU OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", 0, noop, ECU.ALL, True), ] __mode7__ = [ - # sensor name description cmd bytes decoder ECU + # name description cmd bytes decoder ECU OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", 0, dtc, ECU.ALL, True), ] ''' -Assemble the command tables by mode, and allow access by sensor name +Assemble the command tables by mode, and allow access by name ''' class Commands(): @@ -195,7 +195,7 @@ def __init__(self): __mode7__ ] - # allow commands to be accessed by sensor name + # allow commands to be accessed by name for m in self.modes: for c in m: self.__dict__[c.name] = c From 4371ae022fb40778ccc9ce017cd8ba165d6f8806 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 2 Jun 2015 14:52:26 -0400 Subject: [PATCH 245/569] changed tags in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 02f47254..c2ca6d1e 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ "Topic :: System :: Logging", "Intended Audience :: Developers", ], - keywords="obd obd-II obd-ii obd2 car serial vehicle diagnostic", + keywords="obd obdii obd-ii obd2 car serial vehicle diagnostic", author="Brendan Whitfield", author_email="brendanw@windworksdesign.com", url="http://github.com/brendanwhitfield/python-OBD", From 271a19e524bc68e2375e1828d4db6b843d053e72 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 4 Jun 2015 00:05:31 -0400 Subject: [PATCH 246/569] added tests for ECU and ECU_Map --- tests/test_protocol.py | 55 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/test_protocol.py diff --git a/tests/test_protocol.py b/tests/test_protocol.py new file mode 100644 index 00000000..c30c19b3 --- /dev/null +++ b/tests/test_protocol.py @@ -0,0 +1,55 @@ + +import random +from obd.protocols import * +from obd.protocols.protocol import Message, ECU_Map + + +def test_ECU(): + # make sure none of the ECU ID values overlap + tested = [] + + # NOTE: does't include ECU.ALL + for ecu in [ECU.UNKNOWN, ECU.ENGINE, ECU.TRANSMISSION]: + assert (ECU.ALL & ecu) > 0, "ECU: %d is not included in ECU.ALL" % ecu + + for other_ecu in tested: + assert (ecu & other_ecu) == 0, "ECU: %d has a conflicting bit with another ECU constant" %ecu + + tested.append(ecu) + + + +def test_ECU_Map(): + + # test simple default map + e = ECU_Map({ + 0 : 0, + 1 : 10, + 2 : 20 + }) + + for tx_id in range(3): + ecu = (tx_id * 10) + assert e.resolve(tx_id) == ecu, "ECU_Map.resolve() failed" + assert e.lookup(ecu) == tx_id, "ECU_Map.lookup() failed" + + # test undefined tx_ids + assert e.resolve(3) == ECU.UNKNOWN, "ECU_Map.resolve() did not return ECU.UNKNOWN for undefined tx_id" + + # test tx_id writting + e.set(3, 30) + assert e.resolve(3) == 30, "ECU_Map.set() failed after resolve()" + assert e.lookup(30) == 3, "ECU_Map.set() failed after lookup()" + + # test tx_id overwritting + e.set(3, 300) + assert e.resolve(3) == 300, "ECU_Map.set() failed after overwrite" + assert e.lookup(300) == 3, "ECU_Map.set() failed after overwrite" + + # test conflicting ECU ID values + # no two tx_ids should resolve to the same ECU ID + e.set(3, 0) # should cause tx_id 0 become ECU.UNKNOWN + assert e.resolve(3) == 0, "ECU_Map.set() after conflicting overwrite" + assert e.lookup(0) == 3, "ECU_Map.set() after conflicting overwrite" + assert e.resolve(0) == ECU.UNKNOWN + From c35cdddeb2130986e1bef441c5ac5279e4984ab7 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 4 Jun 2015 00:39:44 -0400 Subject: [PATCH 247/569] added tests for basic protocol filtering and moved populate_ecu_map --- tests/test_OBD.py | 4 +-- tests/test_elm327.py | 19 ----------- tests/test_protocol.py | 77 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 77 insertions(+), 23 deletions(-) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 88e5b262..7d81cf76 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -13,7 +13,7 @@ def test_is_connected(): # todo - +""" # TODO: rewrite for new protocol architecture def test_query(): # we don't need an actual serial connection @@ -97,7 +97,7 @@ def write(cmd): assert toCar[0] == "0123" assert r.is_null() ''' - +""" def test_load_commands(): pass diff --git a/tests/test_elm327.py b/tests/test_elm327.py index dac54503..781befc6 100644 --- a/tests/test_elm327.py +++ b/tests/test_elm327.py @@ -2,22 +2,3 @@ from obd.protocols import ECU, SAE_J1850_PWM from obd.elm327 import ELM327 - -def test_find_primary_ecu(): - # parse from messages - - # use primary ECU when multiple are present - p = SAE_J1850_PWM(["48 6B 10 41 00 BE 1F B8 11 AA", "48 6B 12 41 00 BE 1F B8 11 AA"]) - assert p.ecu_map.lookup(ECU.ENGINE) == 0x10 - - # use lone responses regardless - p = SAE_J1850_PWM(["48 6B 12 41 00 BE 1F B8 11 AA"]) - assert p.ecu_map.lookup(ECU.ENGINE) == 0x12 - - # if primary ECU is not listed, use response with most PIDs supported - p = SAE_J1850_PWM(["48 6B 12 41 00 BE 1F B8 11 AA", "48 6B 14 41 00 00 00 B8 11 AA"]) - assert p.ecu_map.lookup(ECU.ENGINE) == 0x12 - - # if no messages were received, the defaults stay in place - p = SAE_J1850_PWM([]) - assert p.ecu_map.lookup(ECU.ENGINE) == p.TX_ID_ENGINE diff --git a/tests/test_protocol.py b/tests/test_protocol.py index c30c19b3..af6dd52a 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -1,7 +1,7 @@ import random from obd.protocols import * -from obd.protocols.protocol import Message, ECU_Map +from obd.protocols.protocol import Frame, Message, ECU_Map def test_ECU(): @@ -18,7 +18,6 @@ def test_ECU(): tested.append(ecu) - def test_ECU_Map(): # test simple default map @@ -53,3 +52,77 @@ def test_ECU_Map(): assert e.lookup(0) == 3, "ECU_Map.set() after conflicting overwrite" assert e.resolve(0) == ECU.UNKNOWN + +def test_frame(): + # constructor + f = Frame("asdf") + assert f.raw == "asdf", "Frame failed to accept raw data as __init__ argument" + assert f.priority == None + assert f.addr_mode == None + assert f.rx_id == None + assert f.tx_id == None + assert f.type == None + assert f.seq_index == 0 + assert f.data_len == None + + +def test_message(): + + # constructor + f = Frame("") + f.tx_id = 42 + R = ["asdf"] + F = [f] + m = Message(R, F) + + assert m.raw == R + assert m.frames == F + assert m.tx_id == 42 + assert m.ecu == ECU.UNKNOWN + + +def test_populate_ecu_map(): + # parse from messages + + # use primary ECU when multiple are present + p = SAE_J1850_PWM(["48 6B 10 41 00 BE 1F B8 11 AA", "48 6B 12 41 00 BE 1F B8 11 AA"]) + assert p.ecu_map.lookup(ECU.ENGINE) == 0x10 + + # use lone responses regardless + p = SAE_J1850_PWM(["48 6B 12 41 00 BE 1F B8 11 AA"]) + assert p.ecu_map.lookup(ECU.ENGINE) == 0x12 + + # if primary ECU is not listed, use response with most PIDs supported + p = SAE_J1850_PWM(["48 6B 12 41 00 BE 1F B8 11 AA", "48 6B 14 41 00 00 00 B8 11 AA"]) + assert p.ecu_map.lookup(ECU.ENGINE) == 0x12 + + # if no messages were received, the defaults stay in place + p = SAE_J1850_PWM([]) + assert p.ecu_map.lookup(ECU.ENGINE) == p.TX_ID_ENGINE + + +def test_call_filtering(): + + # test the basic frame construction + p = UnknownProtocol([]) + + f1 = "48 6B 12 41 00 BE 1F B8 11 AA" + f2 = "48 6B 14 41 00 00 00 B8 11 AA" + raw = [f1, f2] + m = p(raw) + assert len(m) == 1 + assert len(m[0].frames) == 2 + assert m[0].raw == raw + assert m[0].frames[0].raw == f1.replace(' ', '') + assert m[0].frames[1].raw == f2.replace(' ', '') + + + # test invalid hex dropping + p = UnknownProtocol([]) + + raw = ["not hex", f2] + m = p(raw) + assert len(m) == 1 + assert len(m[0].frames) == 1 + assert m[0].raw == raw + assert m[0].frames[0].raw == f2.replace(' ', '') From e74489c52b3e339462a8c698e69c8c355a0554d1 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 21 Jun 2015 15:52:06 -0400 Subject: [PATCH 248/569] handled UNABLE TO CONNECT error --- obd/elm327.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 1915f1ec..a313cdea 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -138,12 +138,19 @@ def __init__(self, portname, baudrate): def load_protocol(self): + """ + Attempts communication with the car. + + Upon success, the appropriate protocol parser is loaded, + and this function returns True + """ # -------------- 0100 (first command, SEARCH protocols) -------------- - # TODO: rewrite this using a "wait for prompt character" - # rather than a fixed wait period r0100 = self.__send("0100") + if self.__has_message(r0100, "UNABLE TO CONNECT"): + debug("The ELM could not establish a connection with the car", True) + return False # ------------------- ATDPN (list protocol number) ------------------- r = self.__send("ATDPN") @@ -176,6 +183,13 @@ def __isok(self, lines, expectEcho=False): return len(lines) == 1 and lines[0] == 'OK' + def __has_message(self, lines, message): + for line in lines: + if message in line: + return True + return False + + def __error(self, msg=None): """ handles fatal failures, print debug info and closes serial """ From 8ec6d444a80844c5bcc76cb4d7d5ee4943e00040 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 8 Jul 2015 08:50:27 -0400 Subject: [PATCH 249/569] load_commands should only use OBD.query, close the connection on failure --- obd/obd.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/obd/obd.py b/obd/obd.py index ee098fa7..dc367ef3 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -76,10 +76,10 @@ def __connect(self, portstr, baudrate): debug("Explicit port defined") self.port = ELM327(portstr, baudrate) - # if a connection was made, query for commands + # if the connection failed, close it if self.port.status == SerialStatus.NOT_CONNECTED: debug("Failed to connect") - self.port = None + self.close() def __load_commands(self): @@ -100,7 +100,9 @@ def __load_commands(self): if not self.supports(get): continue - response = self.query(get, force=True) # ask nicely + # when querying, only use the blocking OBD.query() + # prevents problems when query is redefined in a subclass + response = OBD.query(self, get, force=True) # ask nicely if response.is_null(): continue From 52c48b37fef604a1d9e022b16962ebb6055e8da0 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 31 Oct 2015 20:55:54 -0400 Subject: [PATCH 250/569] initial implementation of ATTP fallback --- obd/elm327.py | 88 +++++++++++++++++++++++++++------------ obd/protocols/protocol.py | 8 ++-- 2 files changed, 66 insertions(+), 30 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index a313cdea..716988b0 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -40,14 +40,18 @@ class ELM327: """ - Provides interface for the vehicles primary ECU. + Handles communication with the ELM327 adapter. + After instantiation with a portname (/dev/ttyUSB0, etc...), - the following functions become available: + the following names become available: + Functions: send_and_parse() - port_name() - status() close() + + Properties: + port_name + status """ _SUPPORTED_PROTOCOLS = { @@ -66,6 +70,21 @@ class ELM327: #"C" : None, # user defined 2 } + # used as a fallback, when ATSP0 doesn't cut it + _TRY_PROTOCOL_ORDER = [ + "6", # ISO_15765_4_11bit_500k + "7", # ISO_15765_4_29bit_500k + "1", # SAE_J1850_PWM + "8", # ISO_15765_4_11bit_250k + "9", # ISO_15765_4_29bit_250k + "2", # SAE_J1850_VPW + "3", # ISO_9141_2 + "4", # ISO_14230_4_5baud + "5", # ISO_14230_4_fast + "A", # SAE_J1939 + ] + + def __init__(self, portname, baudrate): """Initializes port by resetting device and gettings supported PIDs. """ @@ -119,59 +138,76 @@ def __init__(self, portname, baudrate): self.__error("ATL0 did not return 'OK'") return - # ---------------------- ATSPA8 (protocol AUTO) ----------------------- - r = self.__send("ATSPA8") - if not self.__isok(r): - self.__error("ATSPA8 did not return 'OK'") - return - + # by now, we've successfuly communicated with the ELM, but not the car + self.__status = SerialStatus.ELM_CONNECTED # try to communicate with the car, and load the correct protocol parser if self.load_protocol(): self.__status = SerialStatus.CAR_CONNECTED + debug("Connection successful") else: - self.__status = SerialStatus.ELM_CONNECTED + debug("Connected to the adapter, but failed to connect to the vehicle", True) # ------------------------------- done ------------------------------- - debug("Connection successful") def load_protocol(self): """ Attempts communication with the car. + If no protocol is specified, then protocols at tried with `ATTP` + Upon success, the appropriate protocol parser is loaded, and this function returns True """ + # -------------- try the ELM's auto protocol mode -------------- + r = self.__send("ATSP0") + # continue, even if this fails + # if not self.__isok(r): + # self.__error("Failed to set protocol to 'Auto'") + # return False + # -------------- 0100 (first command, SEARCH protocols) -------------- r0100 = self.__send("0100") - if self.__has_message(r0100, "UNABLE TO CONNECT"): debug("The ELM could not establish a connection with the car", True) return False # ------------------- ATDPN (list protocol number) ------------------- r = self.__send("ATDPN") - - if not r: - debug("Describe protocol command didn't return", True) + if len(r) != 1: + debug("Failed to retrieve current protocol", True) return False - p = r[0] + p = r[0] # grab the first (and only) line returned # suppress any "automatic" prefix - p = p[1:] if (len(p) > 1 and p.startswith("A")) else p[:-1] + p = p[1:] if (len(p) > 1 and p.startswith("A")) else p - if p not in self._SUPPORTED_PROTOCOLS: - debug("ELM responded with unknown protocol", True) + # check if the protocol is something we know + if p in self._SUPPORTED_PROTOCOLS: + # jackpot, instantiate the corresponding protocol handler + self.__protocol = self._SUPPORTED_PROTOCOLS[p](r0100) + return True + else: + # an unknown protocol + # this is likely because not all adapter/car combinations work + # in "auto" mode. Some respond to ATDPN responded with "0" + debug("ELM responded with unknown protocol. Trying them one-by-one") + + for p in self._TRY_PROTOCOL_ORDER: + r = self.__send("ATTP") + r0100 = self.__send("0100") + if not self.__has_message(r0100, "UNABLE TO CONNECT"): + # success, found the protocol + self.__protocol = self._SUPPORTED_PROTOCOLS[p](r0100) + return True + + # if we've come this far, then we have failed... return False - # instantiate the correct protocol handler - self.__protocol = self._SUPPORTED_PROTOCOLS[p](r0100) - - return True def __isok(self, lines, expectEcho=False): @@ -299,10 +335,10 @@ def __read(self): if not c: if attempts <= 0: - debug("__read() never recieved prompt character") + debug("Failed to read port, giving up") break - debug("__read() found nothing") + debug("Failed to read port, trying again...") attempts -= 1 continue diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 120751f5..488942aa 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -73,7 +73,7 @@ def set(self, tx_id, ecu_id): any old mappings to that ECU ID """ - # nevery store ECU.UNKNOWNs + # never store ECU.UNKNOWNs # this is the only case where multiple tx_ids resolve to the same ECU ID assert ecu_id != ECU.UNKNOWN @@ -149,7 +149,7 @@ def __eq__(self, other): """ Protocol objects are stateless factories for Frames and Messages. -They are __called__ with the raw string response, and return a +They are __called__ with a list of string responses, and return a list of Messages. """ @@ -272,7 +272,7 @@ def parse_frame(self, frame): with the raw string line from the car. Function should return a boolean. If fatal errors were - found, this function should return False (the Frame is dropped). + found, this function should return False, and the Frame will be dropped. """ raise NotImplementedError() @@ -285,6 +285,6 @@ def parse_message(self, message): preloaded with a list of Frame objects. Function should return a boolean. If fatal errors were - found, this function should return False (the Message is dropped). + found, this function should return False, and the Message will be dropped. """ raise NotImplementedError() From e7eca292ef1a92459eb4d59609a6e8899ea6c615 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 31 Oct 2015 21:42:54 -0400 Subject: [PATCH 251/569] reorganized protocol search order --- obd/elm327.py | 10 +++++----- obd/obd.py | 2 +- obd/protocols/protocol.py | 4 +++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 716988b0..574e6c1c 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -72,11 +72,11 @@ class ELM327: # used as a fallback, when ATSP0 doesn't cut it _TRY_PROTOCOL_ORDER = [ + "8", # ISO_15765_4_11bit_250k "6", # ISO_15765_4_11bit_500k - "7", # ISO_15765_4_29bit_500k "1", # SAE_J1850_PWM - "8", # ISO_15765_4_11bit_250k "9", # ISO_15765_4_29bit_250k + "7", # ISO_15765_4_29bit_500k "2", # SAE_J1850_VPW "3", # ISO_9141_2 "4", # ISO_14230_4_5baud @@ -171,9 +171,9 @@ def load_protocol(self): # -------------- 0100 (first command, SEARCH protocols) -------------- r0100 = self.__send("0100") - if self.__has_message(r0100, "UNABLE TO CONNECT"): - debug("The ELM could not establish a connection with the car", True) - return False + # if self.__has_message(r0100, "UNABLE TO CONNECT"): + # debug("The ELM could not establish a connection with the car", True) + # return False # ------------------- ATDPN (list protocol number) ------------------- r = self.__send("ATDPN") diff --git a/obd/obd.py b/obd/obd.py index dc367ef3..cea7afb9 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -78,7 +78,7 @@ def __connect(self, portstr, baudrate): # if the connection failed, close it if self.port.status == SerialStatus.NOT_CONNECTED: - debug("Failed to connect") + # the ELM327 class will report its own errors self.close() diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 488942aa..55e5f013 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -236,7 +236,9 @@ def populate_ecu_map(self, messages): """ Given a list of messages from different ECUS, (in response to the 0100 PID listing command) - associate each tx_id to an ECU ID constant + associate each tx_id to an ECU ID constant. + + Right now, this just picks the which ECU is the engine. """ if len(messages) == 0: From 7c204bd9b2e7e41f98bd0869fcafd4993fe1e5fa Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 31 Oct 2015 22:03:36 -0400 Subject: [PATCH 252/569] actually send protocol to try --- obd/elm327.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 574e6c1c..599ce74d 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -72,11 +72,11 @@ class ELM327: # used as a fallback, when ATSP0 doesn't cut it _TRY_PROTOCOL_ORDER = [ - "8", # ISO_15765_4_11bit_250k "6", # ISO_15765_4_11bit_500k + "8", # ISO_15765_4_11bit_250k "1", # SAE_J1850_PWM - "9", # ISO_15765_4_29bit_250k "7", # ISO_15765_4_29bit_500k + "9", # ISO_15765_4_29bit_250k "2", # SAE_J1850_VPW "3", # ISO_9141_2 "4", # ISO_14230_4_5baud @@ -198,7 +198,7 @@ def load_protocol(self): debug("ELM responded with unknown protocol. Trying them one-by-one") for p in self._TRY_PROTOCOL_ORDER: - r = self.__send("ATTP") + r = self.__send("ATTP%s" % p) r0100 = self.__send("0100") if not self.__has_message(r0100, "UNABLE TO CONNECT"): # success, found the protocol From cd738ebf59edb22283ad178e4df4f39dade93cc9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 31 Oct 2015 22:16:59 -0400 Subject: [PATCH 253/569] fixed name of OBDResponse after rebase --- obd/async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/async.py b/obd/async.py index 393c817a..149d9947 100644 --- a/obd/async.py +++ b/obd/async.py @@ -140,7 +140,7 @@ def watch(self, c, callback=None, force=False): # new command being watched, store the command if c not in self.__commands: debug("Watching command: %s" % str(c)) - self.__commands[c] = Response() # give it an initial value + self.__commands[c] = OBDResponse() # give it an initial value self.__callbacks[c] = [] # create an empty list # if a callback was given, push it From 2d57af3f7786b68cc769d35cd2692c27a4de646c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 31 Oct 2015 23:17:26 -0400 Subject: [PATCH 254/569] simplified ascii_to_bytes --- obd/__init__.py | 2 +- obd/utils.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/obd/__init__.py b/obd/__init__.py index 97bb579f..10c80307 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -41,7 +41,7 @@ from .async import Async from .commands import commands from .OBDCommand import OBDCommand +from .OBDResponse import OBDResponse, Unit from .protocols import ECU -from .OBDResponse import Unit from .utils import scanSerial, SerialStatus from .debug import debug diff --git a/obd/utils.py b/obd/utils.py index b9264a8a..623d7683 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -46,11 +46,6 @@ class SerialStatus: -def ascii_to_bytes(a): - b = [] - for i in range(0, len(a), 2): - b.append(int(a[i:i+2], 16)) - return b def numBitsSet(n): # TODO: there must be a better way to do this... @@ -68,6 +63,10 @@ def unhex(_hex): def unbin(_bin): return int(_bin, 2) +def ascii_to_bytes(a): + """ converts a string of hex to an array of integer byte values """ + return [ unhex(a[i:i+2]) for i in range(0, len(a), 2) ] + def bitstring(_hex, bits=None): b = bin(unhex(_hex))[2:] if bits is not None: From 395b84d404e887fbf5c65911274e99148aa1bbd5 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 31 Oct 2015 23:27:05 -0400 Subject: [PATCH 255/569] added error string for no devices found --- obd/obd.py | 4 ++++ obd/utils.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/obd/obd.py b/obd/obd.py index cea7afb9..250f774f 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -66,6 +66,10 @@ def __connect(self, portstr, baudrate): portnames = scanSerial() debug("Available ports: " + str(portnames)) + if not portnames: + debug("No OBD-II adapters found", True) + return + for port in portnames: debug("Attempting to use port: " + str(port)) self.port = ELM327(port, baudrate) diff --git a/obd/utils.py b/obd/utils.py index 623d7683..20ef4402 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -83,7 +83,7 @@ def twos_comp(val, num_bits): return val def isHex(_hex): - return all(c in string.hexdigits for c in _hex) + return all([c in string.hexdigits for c in _hex]) def constrainHex(_hex, b): """pads or chops hex to the requested number of bytes""" From a989ede2fc511d54a146769730c3c8104363c7b4 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Nov 2015 00:03:52 -0400 Subject: [PATCH 256/569] ECU_Map was needlessly complex --- obd/protocols/protocol.py | 100 ++++++++++++-------------------------- 1 file changed, 31 insertions(+), 69 deletions(-) diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 55e5f013..f9fe5ded 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -51,62 +51,6 @@ class ECU: TRANSMISSION = 0b00000100 -class ECU_Map: - """ correlation of tx_id to ECU constants above """ - - def __init__(self, init_map): - self.forward_map = init_map # tx_id ---> ECU ID - self.backward_map = {} # ECU ID ---> tx_id - - # the backwards map is simply used to check for ECU ID collisions - # since, for example, it shouldn't be possible to have two - # tx_id's represent the engine. - - # construct the backwards map - for key in self.forward_map: - value = self.forward_map[key] - self.backward_map[value] = key - - def set(self, tx_id, ecu_id): - """ - maps a tx_id to an ECU ID, and removes - any old mappings to that ECU ID - """ - - # never store ECU.UNKNOWNs - # this is the only case where multiple tx_ids resolve to the same ECU ID - assert ecu_id != ECU.UNKNOWN - - # check the backwards map to see if this ECU ID was already registered - if ecu_id in self.backward_map: - # if so, unregister the old mapping - old_tx_id = self.backward_map[ecu_id] - del self.forward_map[old_tx_id] - del self.backward_map[ecu_id] - - # record the new mapping - self.forward_map[tx_id] = ecu_id - self.backward_map[ecu_id] = tx_id - - def resolve(self, tx_id): - """ converts a tx_id into an ECU ID constant """ - if tx_id in self.forward_map: - return self.forward_map[tx_id] - else: - return ECU.UNKNOWN - - def lookup(self, ecu_id): - """ - converts an ECU ID constant into a tx_id - (mostly for testing) - """ - if ecu_id in self.backward_map: - return self.backward_map[ecu_id] - else: - return None - - - class Frame(object): def __init__(self, raw): self.raw = raw @@ -169,12 +113,11 @@ def __init__(self, lines_0100): """ # create the default map - self.ecu_map = ECU_Map({ - self.TX_ID_ENGINE : ECU.ENGINE - }) + # for example: self.TX_ID_ENGINE : ECU.ENGINE + self.ecu_map = {} # parse the 0100 data into messages - # NOTE: at this point, their "ecu" property will be UKNOWN + # NOTE: at this point, their "ecu" property will be UNKNOWN messages = self(lines_0100) # read the messages and assemble the map @@ -227,7 +170,10 @@ def __call__(self, lines): # subclass function to assemble frames into Messages if self.parse_message(message): messages.append(message) - message.ecu = self.ecu_map.resolve(ecu) # mark with the appropriate ECU ID + if ecu in self.ecu_map: + message.ecu = self.ecu_map[ecu] # mark with the appropriate ECU ID + else: + message.ecu = ECU.UNKNOWN return messages @@ -238,21 +184,32 @@ def populate_ecu_map(self, messages): (in response to the 0100 PID listing command) associate each tx_id to an ECU ID constant. - Right now, this just picks the which ECU is the engine. + This is mostly concerned with finding the engine. """ if len(messages) == 0: pass elif len(messages) == 1: # if there's only one response, mark it as the engine regardless - self.ecu_map.set(messages[0].tx_id, ECU.ENGINE) + self.ecu_map[messages[0].tx_id] = ECU.ENGINE else: - # if none of the messages correspond to the engine, - test = lambda m: m.tx_id == self.TX_ID_ENGINE - if not bool([m for m in messages if test(m)]): - # last resort solution, choose ECU - # with the most bits set (most PIDs supported) + # the engine is important + # if we can't find it, we'll use a fallback + found_engine = False + + # if any tx_ids are exact matches to the expected values, record them + for m in messages: + if m.tx_id == self.TX_ID_ENGINE: + self.ecu_map[m.tx_id] = ECU.ENGINE + found_engine = True + # TODO: program more of these when we figure out their constants + # elif m.tx_id == self.TX_ID_TRANSMISSION: + # self.ecu_map[m.tx_id] = ECU.TRANSMISSION + + if not found_engine: + # last resort solution, choose ECU with the most bits set + # (most PIDs supported) to be the engine best = 0 tx_id = None @@ -263,7 +220,12 @@ def populate_ecu_map(self, messages): best = bits tx_id = message.tx_id - self.ecu_map.set(tx_id, ECU.ENGINE) + self.ecu_map[tx_id] = ECU.ENGINE + + # any remaining tx_ids are unknown + for m in messages: + if m.tx_id not in self.ecu_map: + self.ecu_map[m.tx_id] = ECU.UNKNOWN def parse_frame(self, frame): From 2c7828ebd103da4fe20b3292b5595a07b214b061 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Nov 2015 00:11:29 -0400 Subject: [PATCH 257/569] fixed tests directly accessing the old ECU_Map --- tests/test_protocol.py | 47 ++++++------------------------------------ 1 file changed, 6 insertions(+), 41 deletions(-) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index af6dd52a..ffe89698 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -1,7 +1,7 @@ import random from obd.protocols import * -from obd.protocols.protocol import Frame, Message, ECU_Map +from obd.protocols.protocol import Frame, Message def test_ECU(): @@ -18,41 +18,6 @@ def test_ECU(): tested.append(ecu) -def test_ECU_Map(): - - # test simple default map - e = ECU_Map({ - 0 : 0, - 1 : 10, - 2 : 20 - }) - - for tx_id in range(3): - ecu = (tx_id * 10) - assert e.resolve(tx_id) == ecu, "ECU_Map.resolve() failed" - assert e.lookup(ecu) == tx_id, "ECU_Map.lookup() failed" - - # test undefined tx_ids - assert e.resolve(3) == ECU.UNKNOWN, "ECU_Map.resolve() did not return ECU.UNKNOWN for undefined tx_id" - - # test tx_id writting - e.set(3, 30) - assert e.resolve(3) == 30, "ECU_Map.set() failed after resolve()" - assert e.lookup(30) == 3, "ECU_Map.set() failed after lookup()" - - # test tx_id overwritting - e.set(3, 300) - assert e.resolve(3) == 300, "ECU_Map.set() failed after overwrite" - assert e.lookup(300) == 3, "ECU_Map.set() failed after overwrite" - - # test conflicting ECU ID values - # no two tx_ids should resolve to the same ECU ID - e.set(3, 0) # should cause tx_id 0 become ECU.UNKNOWN - assert e.resolve(3) == 0, "ECU_Map.set() after conflicting overwrite" - assert e.lookup(0) == 3, "ECU_Map.set() after conflicting overwrite" - assert e.resolve(0) == ECU.UNKNOWN - - def test_frame(): # constructor f = Frame("asdf") @@ -86,19 +51,19 @@ def test_populate_ecu_map(): # use primary ECU when multiple are present p = SAE_J1850_PWM(["48 6B 10 41 00 BE 1F B8 11 AA", "48 6B 12 41 00 BE 1F B8 11 AA"]) - assert p.ecu_map.lookup(ECU.ENGINE) == 0x10 + assert p.ecu_map[0x10] == ECU.ENGINE # use lone responses regardless p = SAE_J1850_PWM(["48 6B 12 41 00 BE 1F B8 11 AA"]) - assert p.ecu_map.lookup(ECU.ENGINE) == 0x12 + assert p.ecu_map[0x12] == ECU.ENGINE # if primary ECU is not listed, use response with most PIDs supported p = SAE_J1850_PWM(["48 6B 12 41 00 BE 1F B8 11 AA", "48 6B 14 41 00 00 00 B8 11 AA"]) - assert p.ecu_map.lookup(ECU.ENGINE) == 0x12 + assert p.ecu_map[0x12] == ECU.ENGINE - # if no messages were received, the defaults stay in place + # if no messages were received, then the map is empty p = SAE_J1850_PWM([]) - assert p.ecu_map.lookup(ECU.ENGINE) == p.TX_ID_ENGINE + assert len(p.ecu_map) == 0 def test_call_filtering(): From ec8770d5ad3337f7396c3865f6b083e525cdf20d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Nov 2015 00:43:12 -0400 Subject: [PATCH 258/569] fixed test that need its ecu_map trained --- tests/test_OBDCommand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index 6e3eacfb..30280dca 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -37,7 +37,7 @@ def test_clone(): def test_call(): - p = SAE_J1850_PWM([]) + p = SAE_J1850_PWM(["48 6B 10 41 00 FF FF FF FF AA"]) # train the ecu_map to identify the engine messages = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) # parse valid data into response object # valid response size From 206f2ce6aa90820de13fba56aed3146f9b40165d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Nov 2015 00:58:22 -0400 Subject: [PATCH 259/569] added getters for ecus and protocol name --- obd/elm327.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 599ce74d..9815077f 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -50,8 +50,10 @@ class ELM327: close() Properties: - port_name status + port_name + protocol_name + ecus """ _SUPPORTED_PROTOCOLS = { @@ -149,8 +151,6 @@ def __init__(self, portname, baudrate): debug("Connected to the adapter, but failed to connect to the vehicle", True) - # ------------------------------- done ------------------------------- - def load_protocol(self): """ @@ -164,16 +164,9 @@ def load_protocol(self): # -------------- try the ELM's auto protocol mode -------------- r = self.__send("ATSP0") - # continue, even if this fails - # if not self.__isok(r): - # self.__error("Failed to set protocol to 'Auto'") - # return False # -------------- 0100 (first command, SEARCH protocols) -------------- r0100 = self.__send("0100") - # if self.__has_message(r0100, "UNABLE TO CONNECT"): - # debug("The ELM could not establish a connection with the car", True) - # return False # ------------------- ATDPN (list protocol number) ------------------- r = self.__send("ATDPN") @@ -205,8 +198,8 @@ def load_protocol(self): self.__protocol = self._SUPPORTED_PROTOCOLS[p](r0100) return True - # if we've come this far, then we have failed... - return False + # if we've come this far, then we have failed... + return False @@ -243,11 +236,18 @@ def port_name(self): else: return "No Port" - @property def status(self): return self.__status + @property + def ecus(self): + return self.__protocol.ecu_map.values() + + @property + def protocol_name(self): + return self.__protocol.__class__.__name__ + def close(self): """ From c75e4503e5248b5f076792e3c6cfb9bb48a405aa Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Nov 2015 01:16:05 -0500 Subject: [PATCH 260/569] added porcelain commands for ecus and protocol names --- obd/elm327.py | 24 ++++++++---------- obd/obd.py | 42 +++++++++++++++++++++++-------- obd/protocols/protocol.py | 4 +++ obd/protocols/protocol_can.py | 10 ++++++++ obd/protocols/protocol_legacy.py | 10 ++++++++ obd/protocols/protocol_unknown.py | 1 + 6 files changed, 66 insertions(+), 25 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 9815077f..417ef929 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -43,17 +43,14 @@ class ELM327: Handles communication with the ELM327 adapter. After instantiation with a portname (/dev/ttyUSB0, etc...), - the following names become available: + the following functions become available: - Functions: send_and_parse() close() - - Properties: - status - port_name - protocol_name - ecus + status() + port_name() + protocol_name() + ecus() """ _SUPPORTED_PROTOCOLS = { @@ -104,7 +101,7 @@ def __init__(self, portname, baudrate): stopbits = 1, \ bytesize = 8, \ timeout = 3) # seconds - debug("Serial port successfully opened on " + self.port_name) + debug("Serial port successfully opened on " + self.port_name()) except serial.SerialException as e: self.__error(e) @@ -229,24 +226,23 @@ def __error(self, msg=None): debug(' ' + str(msg), True) - @property def port_name(self): if self.__port is not None: return self.__port.portstr else: return "No Port" - @property + def status(self): return self.__status - @property + def ecus(self): return self.__protocol.ecu_map.values() - @property + def protocol_name(self): - return self.__protocol.__class__.__name__ + return self.__protocol.ELM_NAME def close(self): diff --git a/obd/obd.py b/obd/obd.py index 250f774f..ba3f88fb 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -92,7 +92,7 @@ def __load_commands(self): and compiles a list of command objects. """ - if self.status != SerialStatus.CAR_CONNECTED: + if self.status() != SerialStatus.CAR_CONNECTED: debug("Cannot load commands: No connection to car", True) return @@ -144,32 +144,52 @@ def close(self): self.port = None - @property def status(self): if self.port is None: return SerialStatus.NOT_CONNECTED else: - return self.port.status + return self.port.status() - def is_connected(self): - """ - Returns a boolean for whether a connection with the car was made. + def ecus(self): + """ returns a list of ECUs in the vehicle """ + if self.port is None: + return [] + else: + return self.port.ecus() - Note: this function returns False when: - obd.status = SerialStatus.ELM_CONNECTED - """ - return self.status == SerialStatus.CAR_CONNECTED + + def protocol_name(self): + """ returns the name of the protocol being used by the ELM327 """ + if self.port is None: + return "" + else: + return self.port.protocol_name() def get_port_name(self): + print("OBD.get_port_name() is deprecated, use OBD.port_name() instead") + return self.port_name() + + + def port_name(self): """ Returns the name of the currently connected port """ if self.port is not None: - return self.port.port_name + return self.port.port_name() else: return "Not connected to any port" + def is_connected(self): + """ + Returns a boolean for whether a connection with the car was made. + + Note: this function returns False when: + obd.status = SerialStatus.ELM_CONNECTED + """ + return self.status() == SerialStatus.CAR_CONNECTED + + def print_commands(self): """ Utility function meant for working in interactive mode. diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index f9fe5ded..06f0947b 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -101,6 +101,10 @@ def __eq__(self, other): class Protocol(object): # override in subclass for each protocol + + ELM_NAME = "" + ELM_ID = "" + TX_ID_ENGINE = None diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index f95138fa..ea8d6118 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -215,25 +215,35 @@ def parse_message(self, message): class ISO_15765_4_11bit_500k(CANProtocol): + ELM_NAME = "ISO 15765-4 (CAN 11/500)" + ELM_ID = "6" def __init__(self, lines_0100): CANProtocol.__init__(self, lines_0100, id_bits=11) class ISO_15765_4_29bit_500k(CANProtocol): + ELM_NAME = "ISO 15765-4 (CAN 29/500)" + ELM_ID = "7" def __init__(self, lines_0100): CANProtocol.__init__(self, lines_0100, id_bits=29) class ISO_15765_4_11bit_250k(CANProtocol): + ELM_NAME = "ISO 15765-4 (CAN 11/250)" + ELM_ID = "8" def __init__(self, lines_0100): CANProtocol.__init__(self, lines_0100, id_bits=11) class ISO_15765_4_29bit_250k(CANProtocol): + ELM_NAME = "ISO 15765-4 (CAN 29/250)" + ELM_ID = "9" def __init__(self, lines_0100): CANProtocol.__init__(self, lines_0100, id_bits=29) class SAE_J1939(CANProtocol): + ELM_NAME = "SAE J1939 (CAN 29/250)" + ELM_ID = "A" def __init__(self, lines_0100): CANProtocol.__init__(self, lines_0100, id_bits=29) diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index e6c34915..0e28b2b8 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -148,25 +148,35 @@ def parse_message(self, message): class SAE_J1850_PWM(LegacyProtocol): + ELM_NAME = "SAE J1850 PWM" + ELM_ID = "1" def __init__(self, lines_0100): LegacyProtocol.__init__(self, lines_0100) class SAE_J1850_VPW(LegacyProtocol): + ELM_NAME = "SAE J1850 VPW" + ELM_ID = "2" def __init__(self, lines_0100): LegacyProtocol.__init__(self, lines_0100) class ISO_9141_2(LegacyProtocol): + ELM_NAME = "ISO 9141-2" + ELM_ID = "3" def __init__(self, lines_0100): LegacyProtocol.__init__(self, lines_0100) class ISO_14230_4_5baud(LegacyProtocol): + ELM_NAME = "ISO 14230-4 (KWP 5BAUD)" + ELM_ID = "4" def __init__(self, lines_0100): LegacyProtocol.__init__(self, lines_0100) class ISO_14230_4_fast(LegacyProtocol): + ELM_NAME = "ISO 14230-4 (KWP FAST)" + ELM_ID = "5" def __init__(self, lines_0100): LegacyProtocol.__init__(self, lines_0100) diff --git a/obd/protocols/protocol_unknown.py b/obd/protocols/protocol_unknown.py index af9e1204..69fac01a 100644 --- a/obd/protocols/protocol_unknown.py +++ b/obd/protocols/protocol_unknown.py @@ -34,6 +34,7 @@ class UnknownProtocol(Protocol): + """ Class representing an unknown protocol. From 8af97e1a1d690cd73e7c2c5c01788382eeb8b76d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Nov 2015 01:57:06 -0500 Subject: [PATCH 261/569] doc'd new functions, using better constant names and values --- docs/Connections.md | 68 +++++++++++++++++++++++++++++++++++++-- obd/__init__.py | 2 +- obd/elm327.py | 12 +++---- obd/obd.py | 16 ++++----- obd/protocols/protocol.py | 1 + obd/utils.py | 8 ++--- 6 files changed, 86 insertions(+), 21 deletions(-) diff --git a/docs/Connections.md b/docs/Connections.md index be55f7ac..da6f9b0f 100644 --- a/docs/Connections.md +++ b/docs/Connections.md @@ -36,24 +36,88 @@ r = connection.query(obd.commands.RPM) # returns the response from the car --- +### status() + +Returns a string value reflecting the status of the connection. These values should be compared against the `OBDStatus` class. The fact that they are strings is for human readability only. There are currently 3 possible states: + +```python +from obd import OBDStatus + +# no connection is made +OBDStatus.NOT_CONNECTED # "Not Connected" + +# successful communication with the ELM327 adapter +OBDStatus.ELM_CONNECTED # "ELM Connected" + +# successful communication with the vehicle +OBDStatus.CAR_CONNECTED # "Car Connected" +``` + +The middle state, `ELM_CONNECTED` is mostly for diagnosing errors. When a proper connection is established, you will never encounter this value. + +--- + ### is_connected() -Returns a boolean for whether a connection was established. +Returns a boolean for whether a connection was established with the vehicle. It is identical to writing: + +```python +connection.status() == OBDStatus.CAR_CONNECTED +``` --- -### get_port_name() +### port_name() Returns the string name for the currently connected port (`"/dev/ttyUSB0"`). If no connection was made, this function returns `"Not connected to any port"`. --- +### get_port_name() + +**Deprecated:** use `port_name()` instead + +--- + ### supports(command) Returns a boolean for whether a command is supported by both the car and python-OBD --- +### protocol_name() + +Returns the string name of the protocol being used by the adapter. This function does not make any serial requests. The possible values are: + +- `""` when no connection has been made +- `"SAE J1850 PWM"` +- `"SAE J1850 VPW"` +- `"AUTO, ISO 9141-2"` +- `"ISO 14230-4 (KWP 5BAUD)"` +- `"ISO 14230-4 (KWP FAST)"` +- `"ISO 15765-4 (CAN 11/500)"` +- `"ISO 15765-4 (CAN 29/500)"` +- `"ISO 15765-4 (CAN 11/250)"` +- `"ISO 15765-4 (CAN 29/250)"` +- `"SAE J1939 (CAN 29/250)"` + +--- + +### ecus() + +Returns a list of identified "Engine Control Units" visible to the adapter. Each value in the list is a constant representing that ECU's function. These constants are found in the `ECU` class: + +```python +from obd import ECU + +ECU.UNKNOWN +ECU.ENGINE +``` + +Python-OBD can currently only detect the engine computer, but future versions may extend this capability. + +--- + ### close() Closes the connection. diff --git a/obd/__init__.py b/obd/__init__.py index 10c80307..89ab29c6 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -43,5 +43,5 @@ from .OBDCommand import OBDCommand from .OBDResponse import OBDResponse, Unit from .protocols import ECU -from .utils import scanSerial, SerialStatus +from .utils import scanSerial, OBDStatus from .debug import debug diff --git a/obd/elm327.py b/obd/elm327.py index 417ef929..0a14d0e8 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -33,7 +33,7 @@ import serial import time from .protocols import * -from .utils import SerialStatus, numBitsSet +from .utils import OBDStatus, numBitsSet from .debug import debug @@ -87,7 +87,7 @@ class ELM327: def __init__(self, portname, baudrate): """Initializes port by resetting device and gettings supported PIDs. """ - self.__status = SerialStatus.NOT_CONNECTED + self.__status = OBDStatus.NOT_CONNECTED self.__port = None self.__protocol = UnknownProtocol([]) @@ -138,11 +138,11 @@ def __init__(self, portname, baudrate): return # by now, we've successfuly communicated with the ELM, but not the car - self.__status = SerialStatus.ELM_CONNECTED + self.__status = OBDStatus.ELM_CONNECTED # try to communicate with the car, and load the correct protocol parser if self.load_protocol(): - self.__status = SerialStatus.CAR_CONNECTED + self.__status = OBDStatus.CAR_CONNECTED debug("Connection successful") else: debug("Connected to the adapter, but failed to connect to the vehicle", True) @@ -251,7 +251,7 @@ def close(self): attributes to unconnected states. """ - self.__status = SerialStatus.NOT_CONNECTED + self.__status = OBDStatus.NOT_CONNECTED self.__protocol = None if self.__port is not None: @@ -270,7 +270,7 @@ def send_and_parse(self, cmd): Returns a list of Message objects """ - if self.__status == SerialStatus.NOT_CONNECTED: + if self.__status == OBDStatus.NOT_CONNECTED: debug("cannot send_and_parse() when unconnected", True) return None diff --git a/obd/obd.py b/obd/obd.py index ba3f88fb..d029bace 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -35,7 +35,7 @@ from .elm327 import ELM327 from .commands import commands from .OBDResponse import OBDResponse -from .utils import scanSerial, SerialStatus +from .utils import scanSerial, OBDStatus from .debug import debug @@ -74,14 +74,14 @@ def __connect(self, portstr, baudrate): debug("Attempting to use port: " + str(port)) self.port = ELM327(port, baudrate) - if self.port.status >= SerialStatus.ELM_CONNECTED: + if self.port.status >= OBDStatus.ELM_CONNECTED: break # success! stop searching for serial else: debug("Explicit port defined") self.port = ELM327(portstr, baudrate) # if the connection failed, close it - if self.port.status == SerialStatus.NOT_CONNECTED: + if self.port.status == OBDStatus.NOT_CONNECTED: # the ELM327 class will report its own errors self.close() @@ -92,7 +92,7 @@ def __load_commands(self): and compiles a list of command objects. """ - if self.status() != SerialStatus.CAR_CONNECTED: + if self.status() != OBDStatus.CAR_CONNECTED: debug("Cannot load commands: No connection to car", True) return @@ -146,7 +146,7 @@ def close(self): def status(self): if self.port is None: - return SerialStatus.NOT_CONNECTED + return OBDStatus.NOT_CONNECTED else: return self.port.status() @@ -185,9 +185,9 @@ def is_connected(self): Returns a boolean for whether a connection with the car was made. Note: this function returns False when: - obd.status = SerialStatus.ELM_CONNECTED + obd.status = OBDStatus.ELM_CONNECTED """ - return self.status() == SerialStatus.CAR_CONNECTED + return self.status() == OBDStatus.CAR_CONNECTED def print_commands(self): @@ -213,7 +213,7 @@ def query(self, cmd, force=False): protects against sending unsupported commands. """ - if self.status == SerialStatus.NOT_CONNECTED: + if self.status == OBDStatus.NOT_CONNECTED: debug("Query failed, no connection available", True) return OBDResponse() diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 06f0947b..a0622ccf 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -44,6 +44,7 @@ class ECU: """ constant flags used for marking and filtering messages """ ALL = 0b11111111 # used by OBDCommands to accept messages from any ECU + ALL_KNOWN = 0b11111110 # used to ignore unknown ECUs, since this lib probably can't handle them # each ECU gets its own bit for ease of making OR filters UNKNOWN = 0b00000001 # unknowns get their own bit, since they need to be accepted by the ALL filter diff --git a/obd/utils.py b/obd/utils.py index 20ef4402..0ff9f322 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -37,12 +37,12 @@ from .debug import debug -class SerialStatus: +class OBDStatus: """ Values for the connection status flags """ - NOT_CONNECTED = 0 - ELM_CONNECTED = 1 - CAR_CONNECTED = 2 + NOT_CONNECTED = "Not Connected" + ELM_CONNECTED = "ELM Connected" + CAR_CONNECTED = "Car Connected" From b5ab69a24c48d4a810777910acdd936275f5db5e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Nov 2015 13:26:25 -0500 Subject: [PATCH 262/569] implemented fast mode --- obd/OBDCommand.py | 16 +++- obd/commands.py | 214 +++++++++++++++++++++++----------------------- obd/elm327.py | 4 +- obd/obd.py | 36 ++++++-- 4 files changed, 153 insertions(+), 117 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 2c2ecbbe..ce083295 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -31,17 +31,27 @@ from .utils import * from .debug import debug +from .protocols import ECU from .OBDResponse import OBDResponse class OBDCommand(): - def __init__(self, name, desc, command, returnBytes, decoder, ecu, supported=False): + def __init__(self, + name, + desc, + command, + returnBytes, + decoder, + ecu=ECU.ALL, + fast=False, + supported=False): self.name = name # human readable name (also used as key in commands dict) self.desc = desc # human readable description self.command = command # command string self.bytes = returnBytes # number of bytes expected in return self.decode = decoder # decoding function self.ecu = ecu # ECU ID from which this command expects messages from + self.fast = fast # can an extra digit be added to the end of the command? (to make the ELM return early) self.supported = supported # bool for support def clone(self): @@ -50,7 +60,9 @@ def clone(self): self.command, self.bytes, self.decode, - self.ecu) + self.ecu, + self.fast, + self.supported) @property def mode_int(self): diff --git a/obd/commands.py b/obd/commands.py index 76c3f2bf..7f798be5 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -45,107 +45,107 @@ # NOTE: commands MUST be in PID order, one command per PID (for fast lookup using __mode1__[pid]) __mode1__ = [ - # name description cmd bytes decoder ECU - OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "0100", 4, pid, ECU.ALL , True), # the first PID getter is assumed to be supported - OBDCommand("STATUS" , "Status since DTCs cleared" , "0101", 4, status, ECU.ENGINE), - OBDCommand("FREEZE_DTC" , "Freeze DTC" , "0102", 2, noop, ECU.ENGINE), - OBDCommand("FUEL_STATUS" , "Fuel System Status" , "0103", 2, fuel_status, ECU.ENGINE), - OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , "0104", 1, percent, ECU.ENGINE), - OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "0105", 1, temp, ECU.ENGINE), - OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "0106", 1, percent_centered, ECU.ENGINE), - OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "0107", 1, percent_centered, ECU.ENGINE), - OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "0108", 1, percent_centered, ECU.ENGINE), - OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "0109", 1, percent_centered, ECU.ENGINE), - OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "010A", 1, fuel_pressure, ECU.ENGINE), - OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "010B", 1, pressure, ECU.ENGINE), - OBDCommand("RPM" , "Engine RPM" , "010C", 2, rpm, ECU.ENGINE), - OBDCommand("SPEED" , "Vehicle Speed" , "010D", 1, speed, ECU.ENGINE), - OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "010E", 1, timing_advance, ECU.ENGINE), - OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "010F", 1, temp, ECU.ENGINE), - OBDCommand("MAF" , "Air Flow Rate (MAF)" , "0110", 2, maf, ECU.ENGINE), - OBDCommand("THROTTLE_POS" , "Throttle Position" , "0111", 1, percent, ECU.ENGINE), - OBDCommand("AIR_STATUS" , "Secondary Air Status" , "0112", 1, air_status, ECU.ENGINE), - OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "0113", 1, noop, ECU.ENGINE), - OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , "0114", 2, sensor_voltage, ECU.ENGINE), - OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , "0115", 2, sensor_voltage, ECU.ENGINE), - OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , "0116", 2, sensor_voltage, ECU.ENGINE), - OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , "0117", 2, sensor_voltage, ECU.ENGINE), - OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , "0118", 2, sensor_voltage, ECU.ENGINE), - OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , "0119", 2, sensor_voltage, ECU.ENGINE), - OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , "011A", 2, sensor_voltage, ECU.ENGINE), - OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , "011B", 2, sensor_voltage, ECU.ENGINE), - OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "011C", 1, obd_compliance, ECU.ENGINE), - OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "011D", 1, noop, ECU.ENGINE), - OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "011E", 1, noop, ECU.ENGINE), - OBDCommand("RUN_TIME" , "Engine Run Time" , "011F", 2, seconds, ECU.ENGINE), - - # name description cmd bytes decoder ECU - OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "0120", 4, pid, ECU.ALL ), - OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , "0121", 2, distance, ECU.ENGINE), - OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , "0122", 2, fuel_pres_vac, ECU.ENGINE), - OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , "0123", 2, fuel_pres_direct, ECU.ENGINE), - OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , "0124", 4, sensor_voltage_big, ECU.ENGINE), - OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , "0125", 4, sensor_voltage_big, ECU.ENGINE), - OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , "0126", 4, sensor_voltage_big, ECU.ENGINE), - OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , "0127", 4, sensor_voltage_big, ECU.ENGINE), - OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , "0128", 4, sensor_voltage_big, ECU.ENGINE), - OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , "0129", 4, sensor_voltage_big, ECU.ENGINE), - OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , "012A", 4, sensor_voltage_big, ECU.ENGINE), - OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , "012B", 4, sensor_voltage_big, ECU.ENGINE), - OBDCommand("COMMANDED_EGR" , "Commanded EGR" , "012C", 1, percent, ECU.ENGINE), - OBDCommand("EGR_ERROR" , "EGR Error" , "012D", 1, percent_centered, ECU.ENGINE), - OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , "012E", 1, percent, ECU.ENGINE), - OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , "012F", 1, percent, ECU.ENGINE), - OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , "0130", 1, count, ECU.ENGINE), - OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , "0131", 2, distance, ECU.ENGINE), - OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , "0132", 2, evap_pressure, ECU.ENGINE), - OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , "0133", 1, pressure, ECU.ENGINE), - OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , "0134", 4, current_centered, ECU.ENGINE), - OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , "0135", 4, current_centered, ECU.ENGINE), - OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , "0136", 4, current_centered, ECU.ENGINE), - OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , "0137", 4, current_centered, ECU.ENGINE), - OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , "0138", 4, current_centered, ECU.ENGINE), - OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , "0139", 4, current_centered, ECU.ENGINE), - OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , "013A", 4, current_centered, ECU.ENGINE), - OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , "013B", 4, current_centered, ECU.ENGINE), - OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , "013C", 2, catalyst_temp, ECU.ENGINE), - OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , "013D", 2, catalyst_temp, ECU.ENGINE), - OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , "013E", 2, catalyst_temp, ECU.ENGINE), - OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , "013F", 2, catalyst_temp, ECU.ENGINE), - - # name description cmd bytes decoder ECU - OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "0140", 4, pid, ECU.ALL ), - OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "0141", 4, todo, ECU.ENGINE), - OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "0142", 2, todo, ECU.ENGINE), - OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "0143", 2, todo, ECU.ENGINE), - OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "0144", 2, todo, ECU.ENGINE), - OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , "0145", 1, percent, ECU.ENGINE), - OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , "0146", 1, temp, ECU.ENGINE), - OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , "0147", 1, percent, ECU.ENGINE), - OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , "0148", 1, percent, ECU.ENGINE), - OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , "0149", 1, percent, ECU.ENGINE), - OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , "014A", 1, percent, ECU.ENGINE), - OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , "014B", 1, percent, ECU.ENGINE), - OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , "014C", 1, percent, ECU.ENGINE), - OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , "014D", 2, minutes, ECU.ENGINE), - OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , "014E", 2, minutes, ECU.ENGINE), - OBDCommand("MAX_VALUES" , "Various Max values" , "014F", 4, noop, ECU.ENGINE), # todo: decode this - OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , "0150", 4, max_maf, ECU.ENGINE), - OBDCommand("FUEL_TYPE" , "Fuel Type" , "0151", 1, fuel_type, ECU.ENGINE), - OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , "0152", 1, percent, ECU.ENGINE), - OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , "0153", 2, abs_evap_pressure, ECU.ENGINE), - OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , "0154", 2, evap_pressure_alt, ECU.ENGINE), - OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , "0155", 2, percent_centered, ECU.ENGINE), # todo: decode seconds value for banks 3 and 4 - OBDCommand("LONG_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , "0156", 2, percent_centered, ECU.ENGINE), - OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , "0157", 2, percent_centered, ECU.ENGINE), - OBDCommand("LONG_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , "0158", 2, percent_centered, ECU.ENGINE), - OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , "0159", 2, fuel_pres_direct, ECU.ENGINE), - OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , "015A", 1, percent, ECU.ENGINE), - OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , "015B", 1, percent, ECU.ENGINE), - OBDCommand("OIL_TEMP" , "Engine oil temperature" , "015C", 1, temp, ECU.ENGINE), - OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , "015D", 2, inject_timing, ECU.ENGINE), - OBDCommand("FUEL_RATE" , "Engine fuel rate" , "015E", 2, fuel_rate, ECU.ENGINE), - OBDCommand("EMISSION_REQ" , "Designed emission requirements" , "015F", 1, noop, ECU.ENGINE), + # name description cmd bytes decoder ECU fast + OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "0100", 4, pid, ECU.ENGINE, True, True), # the first PID getter is assumed to be supported + OBDCommand("STATUS" , "Status since DTCs cleared" , "0101", 4, status, ECU.ENGINE, True), + OBDCommand("FREEZE_DTC" , "Freeze DTC" , "0102", 2, noop, ECU.ENGINE, True), + OBDCommand("FUEL_STATUS" , "Fuel System Status" , "0103", 2, fuel_status, ECU.ENGINE, True), + OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , "0104", 1, percent, ECU.ENGINE, True), + OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "0105", 1, temp, ECU.ENGINE, True), + OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "0106", 1, percent_centered, ECU.ENGINE, True), + OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "0107", 1, percent_centered, ECU.ENGINE, True), + OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "0108", 1, percent_centered, ECU.ENGINE, True), + OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "0109", 1, percent_centered, ECU.ENGINE, True), + OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "010A", 1, fuel_pressure, ECU.ENGINE, True), + OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "010B", 1, pressure, ECU.ENGINE, True), + OBDCommand("RPM" , "Engine RPM" , "010C", 2, rpm, ECU.ENGINE, True), + OBDCommand("SPEED" , "Vehicle Speed" , "010D", 1, speed, ECU.ENGINE, True), + OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "010E", 1, timing_advance, ECU.ENGINE, True), + OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "010F", 1, temp, ECU.ENGINE, True), + OBDCommand("MAF" , "Air Flow Rate (MAF)" , "0110", 2, maf, ECU.ENGINE, True), + OBDCommand("THROTTLE_POS" , "Throttle Position" , "0111", 1, percent, ECU.ENGINE, True), + OBDCommand("AIR_STATUS" , "Secondary Air Status" , "0112", 1, air_status, ECU.ENGINE, True), + OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "0113", 1, noop, ECU.ENGINE, True), + OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , "0114", 2, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , "0115", 2, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , "0116", 2, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , "0117", 2, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , "0118", 2, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , "0119", 2, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , "011A", 2, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , "011B", 2, sensor_voltage, ECU.ENGINE, True), + OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "011C", 1, obd_compliance, ECU.ENGINE, True), + OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "011D", 1, noop, ECU.ENGINE, True), + OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "011E", 1, noop, ECU.ENGINE, True), + OBDCommand("RUN_TIME" , "Engine Run Time" , "011F", 2, seconds, ECU.ENGINE, True), + + # name description cmd bytes decoder ECU fast + OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "0120", 4, pid, ECU.ENGINE, True), + OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , "0121", 2, distance, ECU.ENGINE, True), + OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , "0122", 2, fuel_pres_vac, ECU.ENGINE, True), + OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , "0123", 2, fuel_pres_direct, ECU.ENGINE, True), + OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , "0124", 4, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , "0125", 4, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , "0126", 4, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , "0127", 4, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , "0128", 4, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , "0129", 4, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , "012A", 4, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , "012B", 4, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("COMMANDED_EGR" , "Commanded EGR" , "012C", 1, percent, ECU.ENGINE, True), + OBDCommand("EGR_ERROR" , "EGR Error" , "012D", 1, percent_centered, ECU.ENGINE, True), + OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , "012E", 1, percent, ECU.ENGINE, True), + OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , "012F", 1, percent, ECU.ENGINE, True), + OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , "0130", 1, count, ECU.ENGINE, True), + OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , "0131", 2, distance, ECU.ENGINE, True), + OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , "0132", 2, evap_pressure, ECU.ENGINE, True), + OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , "0133", 1, pressure, ECU.ENGINE, True), + OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , "0134", 4, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , "0135", 4, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , "0136", 4, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , "0137", 4, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , "0138", 4, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , "0139", 4, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , "013A", 4, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , "013B", 4, current_centered, ECU.ENGINE, True), + OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , "013C", 2, catalyst_temp, ECU.ENGINE, True), + OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , "013D", 2, catalyst_temp, ECU.ENGINE, True), + OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , "013E", 2, catalyst_temp, ECU.ENGINE, True), + OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , "013F", 2, catalyst_temp, ECU.ENGINE, True), + + # name description cmd bytes decoder ECU fast + OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "0140", 4, pid, ECU.ENGINE, True), + OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "0141", 4, todo, ECU.ENGINE, True), + OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "0142", 2, todo, ECU.ENGINE, True), + OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "0143", 2, todo, ECU.ENGINE, True), + OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "0144", 2, todo, ECU.ENGINE, True), + OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , "0145", 1, percent, ECU.ENGINE, True), + OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , "0146", 1, temp, ECU.ENGINE, True), + OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , "0147", 1, percent, ECU.ENGINE, True), + OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , "0148", 1, percent, ECU.ENGINE, True), + OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , "0149", 1, percent, ECU.ENGINE, True), + OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , "014A", 1, percent, ECU.ENGINE, True), + OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , "014B", 1, percent, ECU.ENGINE, True), + OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , "014C", 1, percent, ECU.ENGINE, True), + OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , "014D", 2, minutes, ECU.ENGINE, True), + OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , "014E", 2, minutes, ECU.ENGINE, True), + OBDCommand("MAX_VALUES" , "Various Max values" , "014F", 4, noop, ECU.ENGINE, True), # todo: decode this + OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , "0150", 4, max_maf, ECU.ENGINE, True), + OBDCommand("FUEL_TYPE" , "Fuel Type" , "0151", 1, fuel_type, ECU.ENGINE, True), + OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , "0152", 1, percent, ECU.ENGINE, True), + OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , "0153", 2, abs_evap_pressure, ECU.ENGINE, True), + OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , "0154", 2, evap_pressure_alt, ECU.ENGINE, True), + OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , "0155", 2, percent_centered, ECU.ENGINE, True), # todo: decode seconds value for banks 3 and 4 + OBDCommand("LONG_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , "0156", 2, percent_centered, ECU.ENGINE, True), + OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , "0157", 2, percent_centered, ECU.ENGINE, True), + OBDCommand("LONG_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , "0158", 2, percent_centered, ECU.ENGINE, True), + OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , "0159", 2, fuel_pres_direct, ECU.ENGINE, True), + OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , "015A", 1, percent, ECU.ENGINE, True), + OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , "015B", 1, percent, ECU.ENGINE, True), + OBDCommand("OIL_TEMP" , "Engine oil temperature" , "015C", 1, temp, ECU.ENGINE, True), + OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , "015D", 2, inject_timing, ECU.ENGINE, True), + OBDCommand("FUEL_RATE" , "Engine fuel rate" , "015E", 2, fuel_rate, ECU.ENGINE, True), + OBDCommand("EMISSION_REQ" , "Designed emission requirements" , "015F", 1, noop, ECU.ENGINE, True), ] @@ -160,18 +160,18 @@ __mode3__ = [ - # name description cmd bytes decoder ECU - OBDCommand("GET_DTC" , "Get DTCs" , "03", 0, dtc, ECU.ALL, True), + # name description cmd bytes decoder ECU fast + OBDCommand("GET_DTC" , "Get DTCs" , "03", 0, dtc, ECU.ALL, True, True), ] __mode4__ = [ - # name description cmd bytes decoder ECU - OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", 0, noop, ECU.ALL, True), + # name description cmd bytes decoder ECU fast + OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", 0, noop, ECU.ALL, True, True), ] __mode7__ = [ - # name description cmd bytes decoder ECU - OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", 0, dtc, ECU.ALL, True), + # name description cmd bytes decoder ECU fast + OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", 0, dtc, ECU.ALL, True, True), ] diff --git a/obd/elm327.py b/obd/elm327.py index 0a14d0e8..6403581c 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -84,7 +84,7 @@ class ELM327: ] - def __init__(self, portname, baudrate): + def __init__(self, portname, baudrate, protocol): """Initializes port by resetting device and gettings supported PIDs. """ self.__status = OBDStatus.NOT_CONNECTED @@ -267,6 +267,8 @@ def send_and_parse(self, cmd): Sends the given command string, and parses the response lines with the protocol object. + An empty command string will re-trigger the previous command + Returns a list of Message objects """ diff --git a/obd/obd.py b/obd/obd.py index d029bace..a85c356e 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -46,17 +46,19 @@ class OBD(object): with it's assorted commands/sensors. """ - def __init__(self, portstr=None, baudrate=38400): + def __init__(self, portstr=None, baudrate=38400, protocol=None, fast=True): self.port = None self.supported_commands = [] + self.fast = fast + self.__last_command = "" # used for debug("========================== python-OBD (v%s) ==========================" % __version__) - self.__connect(portstr, baudrate) # initialize by connecting and loading sensors + self.__connect(portstr, baudrate, protocol) # initialize by connecting and loading sensors self.__load_commands() # try to load the car's supported commands debug("=========================================================================") - def __connect(self, portstr, baudrate): + def __connect(self, portstr, baudrate, protocol): """ Attempts to instantiate an ELM327 connection object. """ @@ -72,13 +74,13 @@ def __connect(self, portstr, baudrate): for port in portnames: debug("Attempting to use port: " + str(port)) - self.port = ELM327(port, baudrate) + self.port = ELM327(port, baudrate, protocol) if self.port.status >= OBDStatus.ELM_CONNECTED: break # success! stop searching for serial else: debug("Explicit port defined") - self.port = ELM327(portstr, baudrate) + self.port = ELM327(portstr, baudrate, protocol) # if the connection failed, close it if self.port.status == OBDStatus.NOT_CONNECTED: @@ -105,7 +107,7 @@ def __load_commands(self): continue # when querying, only use the blocking OBD.query() - # prevents problems when query is redefined in a subclass + # prevents problems when query is redefined in a subclass (like Async) response = OBD.query(self, get, force=True) # ask nicely if response.is_null(): @@ -221,12 +223,32 @@ def query(self, cmd, force=False): debug("'%s' is not supported" % str(cmd), True) return OBDResponse() + # send command and retrieve message debug("Sending command: %s" % str(cmd)) - messages = self.port.send_and_parse(cmd.command) + cmd_string = self.__build_command_string(cmd) + messages = self.port.send_and_parse(cmd_string) + + # if we're sending a new command, note it + if cmd_string: + self.__last_command = cmd_string if not messages: debug("No valid OBD Messages returned", True) return OBDResponse() return cmd(messages) # compute a response object + + + def __build_command_string(self, cmd): + """ assembles the appropriate command string """ + cmd_string = cmd.command + + if self.fast and cmd.fast: + cmd_string += str(len(self.ecus())) + + # if we sent this last time, just send + if self.fast and (cmd_string == self.__last_command): + cmd_string = "" + + return cmd_string From 06f2e83a49d7d9111eba52e15c81cd0e5e161ac6 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Nov 2015 13:28:12 -0500 Subject: [PATCH 263/569] don't append ecu counts to DTC handling commands --- obd/commands.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 7f798be5..25575558 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -161,17 +161,17 @@ __mode3__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("GET_DTC" , "Get DTCs" , "03", 0, dtc, ECU.ALL, True, True), + OBDCommand("GET_DTC" , "Get DTCs" , "03", 0, dtc, ECU.ALL, False, True), ] __mode4__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", 0, noop, ECU.ALL, True, True), + OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", 0, noop, ECU.ALL, False, True), ] __mode7__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", 0, dtc, ECU.ALL, True, True), + OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", 0, dtc, ECU.ALL, False, True), ] From 88c496673b640273616d419d129e21dfffaa7663 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Nov 2015 14:04:02 -0500 Subject: [PATCH 264/569] moved ecu lookup to its own function --- obd/commands.py | 2 ++ obd/protocols/protocol.py | 12 ++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 25575558..dfedb4e5 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -44,6 +44,8 @@ # NOTE: the NAME field will be used as the dict key for that sensor # NOTE: commands MUST be in PID order, one command per PID (for fast lookup using __mode1__[pid]) +# see OBDCommand.py for descriptions & purposes for each of these fields + __mode1__ = [ # name description cmd bytes decoder ECU fast OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "0100", 4, pid, ECU.ENGINE, True, True), # the first PID getter is assumed to be supported diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index a0622ccf..1b052052 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -174,15 +174,19 @@ def __call__(self, lines): # subclass function to assemble frames into Messages if self.parse_message(message): + message.ecu = self.lookup_ecu(ecu) # mark with the appropriate ECU ID messages.append(message) - if ecu in self.ecu_map: - message.ecu = self.ecu_map[ecu] # mark with the appropriate ECU ID - else: - message.ecu = ECU.UNKNOWN return messages + def lookup_ecu(self, tx_id): + if tx_id in self.ecu_map: + return self.ecu_map[tx_id] + else: + return ECU.UNKNOWN + + def populate_ecu_map(self, messages): """ Given a list of messages from different ECUS, From 4075b38169270d7354eb13a65e19e6eae2019b2b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Nov 2015 16:10:15 -0500 Subject: [PATCH 265/569] protocols now pass invalid lines as messages --- obd/commands.py | 20 ++++++++++---- obd/protocols/protocol.py | 58 ++++++++++++++++++++++++++++----------- 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index dfedb4e5..51ee134c 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -162,18 +162,23 @@ __mode3__ = [ - # name description cmd bytes decoder ECU fast - OBDCommand("GET_DTC" , "Get DTCs" , "03", 0, dtc, ECU.ALL, False, True), + # name description cmd bytes decoder ECU fast + OBDCommand("GET_DTC" , "Get DTCs" , "03", 0, dtc, ECU.ALL, False, True), ] __mode4__ = [ - # name description cmd bytes decoder ECU fast - OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", 0, noop, ECU.ALL, False, True), + # name description cmd bytes decoder ECU fast + OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", 0, noop, ECU.ALL, False, True), ] __mode7__ = [ - # name description cmd bytes decoder ECU fast - OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", 0, dtc, ECU.ALL, False, True), + # name description cmd bytes decoder ECU fast + OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", 0, dtc, ECU.ALL, False, True), +] + +__special__ = [ + # name description cmd bytes decoder ECU fast + OBDCommand("VOLTAGE" , "Vehicle bettery voltage" , "ATRV", 0, noop, ECU.UNKNOWN, False, True), ] @@ -202,6 +207,9 @@ def __init__(self): for c in m: self.__dict__[c.name] = c + for c in __special__: + self.__dict__[c.name] = c + def __getitem__(self, key): """ diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 1b052052..d6ea05c3 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -53,6 +53,7 @@ class ECU: class Frame(object): + """ represents a single line of OBD output """ def __init__(self, raw): self.raw = raw self.data = [] @@ -66,6 +67,7 @@ def __init__(self, raw): class Message(object): + """ represents a fully parsed OBD message of one or more Frames (lines) """ def __init__(self, raw, frames): self.raw = raw self.frames = frames @@ -93,8 +95,11 @@ def __eq__(self, other): """ -Protocol objects are stateless factories for Frames and Messages. -They are __called__ with a list of string responses, and return a +Protocol objects are factories for Frame and Message objects. They are +largely stateless, with the exception of an ECU tagging system, which +are initialized by passing the response to an "0100" command. + +Protocols are __called__ with a list of string responses, and return a list of Messages. """ @@ -117,7 +122,7 @@ def __init__(self, lines_0100): car to determine the ECU layout. """ - # create the default map + # create the default, empty map # for example: self.TX_ID_ENGINE : ECU.ENGINE self.ecu_map = {} @@ -137,15 +142,28 @@ def __call__(self, lines): accepts a list of raw strings from the car, split by lines """ - # ditch spaces - filtered_lines = [line.replace(' ', '') for line in lines] + # ---------------------------- preprocess ---------------------------- + + # Non-hex (non-OBD) lines shouldn't go through the big parsers, + # since they are typically messages such as: "NO DATA", "CAN ERROR", + # "UNABLE TO CONNECT", etc, so sort them into these two lists: + obd_lines = [] + non_obd_lines = [] + + for line in lines: + + line_no_spaces = line.replace(' ', '') - # ditch frames without valid hex (trashes "NO DATA", etc...) - filtered_lines = filter(isHex, filtered_lines) + if isHex(line_no_spaces): + obd_lines.append(line_no_spaces) + else: + non_obd_lines.append(line) # pass the original, un-scrubbed line + + # ---------------------- handle valid OBD lines ---------------------- # parse each frame (each line) frames = [] - for line in filtered_lines: + for line in obd_lines: frame = Frame(line) @@ -156,27 +174,35 @@ def __call__(self, lines): # group frames by transmitting ECU - # ecus[tx_id] = [Frame, Frame] - ecus = {} + # frames_by_ECU[tx_id] = [Frame, Frame] + frames_by_ECU = {} for frame in frames: - if frame.tx_id not in ecus: - ecus[frame.tx_id] = [frame] + if frame.tx_id not in frames_by_ECU: + frames_by_ECU[frame.tx_id] = [frame] else: - ecus[frame.tx_id].append(frame) + frames_by_ECU[frame.tx_id].append(frame) # parse frames into whole messages messages = [] - for ecu in ecus: + for ecu in frames_by_ECU: # new message object with a copy of the raw data # and frames addressed for this ecu - message = Message(list(lines), ecus[ecu]) + message = Message(list(lines), frames_by_ECU[ecu]) # subclass function to assemble frames into Messages if self.parse_message(message): - message.ecu = self.lookup_ecu(ecu) # mark with the appropriate ECU ID + # mark with the appropriate ECU ID + message.ecu = self.lookup_ecu(ecu) messages.append(message) + # ----------- handle invalid lines (probably from the ELM) ----------- + + for line in non_obd_lines: + # give each line its own message object + # messages are ECU.UNKNOWN by default + messages.append( Message(list(lines), [ Frame(line) ]) ) + return messages From 2010622987a5e6b661572df1a89facb18a725bd1 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Nov 2015 16:28:41 -0500 Subject: [PATCH 266/569] removed the raw string list from the Message object --- obd/OBDResponse.py | 6 +++--- obd/protocols/protocol.py | 11 +++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py index 4c6f8459..fd7309ca 100644 --- a/obd/OBDResponse.py +++ b/obd/OBDResponse.py @@ -64,15 +64,15 @@ class Unit: class OBDResponse(): """ Standard response object for any OBDCommand """ - def __init__(self, command=None, message=None): + def __init__(self, command=None, messages=None): self.command = command - self.message = message + self.messages = messages if messages else [] self.value = None self.unit = Unit.NONE self.time = time.time() def is_null(self): - return (self.message == None) or (self.value == None) + return (not self.messages) or (self.value == None) def __str__(self): if self.unit != Unit.NONE: diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index d6ea05c3..c206c629 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -53,7 +53,7 @@ class ECU: class Frame(object): - """ represents a single line of OBD output """ + """ represents a single parsed line of OBD output """ def __init__(self, raw): self.raw = raw self.data = [] @@ -68,8 +68,7 @@ def __init__(self, raw): class Message(object): """ represents a fully parsed OBD message of one or more Frames (lines) """ - def __init__(self, raw, frames): - self.raw = raw + def __init__(self, frames): self.frames = frames self.ecu = ECU.UNKNOWN self.data = [] @@ -83,7 +82,7 @@ def tx_id(self): def __eq__(self, other): if isinstance(other, Message): - for attr in ["raw", "frames", "ecu", "data"]: + for attr in ["frames", "ecu", "data"]: if getattr(self, attr) != getattr(other, attr): return False return True @@ -188,7 +187,7 @@ def __call__(self, lines): # new message object with a copy of the raw data # and frames addressed for this ecu - message = Message(list(lines), frames_by_ECU[ecu]) + message = Message(frames_by_ECU[ecu]) # subclass function to assemble frames into Messages if self.parse_message(message): @@ -201,7 +200,7 @@ def __call__(self, lines): for line in non_obd_lines: # give each line its own message object # messages are ECU.UNKNOWN by default - messages.append( Message(list(lines), [ Frame(line) ]) ) + messages.append( Message([ Frame(line) ]) ) return messages From 77c9a3493e723e0a70dee2f1b8899f616b58f6cd Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Nov 2015 16:58:13 -0500 Subject: [PATCH 267/569] pre-filter incoming messages, fixed query status check --- obd/OBDCommand.py | 25 +++++++++++++++++-------- obd/commands.py | 6 +++--- obd/obd.py | 2 +- obd/utils.py | 2 +- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index ce083295..2a617799 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -80,23 +80,28 @@ def pid_int(self): def __call__(self, messages): + # filter for applicable messages (from the right ECU(s)) + for_us = lambda m: self.ecu & m.ecu > 0 + messages = list(filter(for_us, messages)) + + # create the response object with the raw data recieved # and reference to original command r = OBDResponse(self, messages) - + + + + # combine the bytes back into a hex string # TODO: rewrite decoders to handle raw byte arrays _data = "" # filter for applicable messages for message in messages: - - # if this command accepts messages from this ECU - if self.ecu & message.ecu > 0: - for b in message.data: - h = hex(b)[2:].upper() - h = "0" + h if len(h) < 2 else h - _data += h + for b in message.data: + h = hex(b)[2:].upper() + h = "0" + h if len(h) < 2 else h + _data += h # constrain number of bytes in response if (self.bytes > 0): # zero bytes means flexible response @@ -109,6 +114,10 @@ def __call__(self, messages): return r + def __constrain_message_data(self, message): + """ pads or chops the data field to the size specified by this command """ + pass + def __str__(self): return "%s: %s" % (self.command, self.desc) diff --git a/obd/commands.py b/obd/commands.py index 51ee134c..ae200e44 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -176,9 +176,9 @@ OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", 0, dtc, ECU.ALL, False, True), ] -__special__ = [ +__misc__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("VOLTAGE" , "Vehicle bettery voltage" , "ATRV", 0, noop, ECU.UNKNOWN, False, True), + OBDCommand("VOLTAGE" , "Voltage detected by OBD-II adapter" , "ATRV", 0, noop, ECU.UNKNOWN, False, True), ] @@ -207,7 +207,7 @@ def __init__(self): for c in m: self.__dict__[c.name] = c - for c in __special__: + for c in __misc__: self.__dict__[c.name] = c diff --git a/obd/obd.py b/obd/obd.py index a85c356e..847a8d27 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -215,7 +215,7 @@ def query(self, cmd, force=False): protects against sending unsupported commands. """ - if self.status == OBDStatus.NOT_CONNECTED: + if self.status() == OBDStatus.NOT_CONNECTED: debug("Query failed, no connection available", True) return OBDResponse() diff --git a/obd/utils.py b/obd/utils.py index 0ff9f322..16b32c36 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -98,8 +98,8 @@ def constrainHex(_hex, b): return _hex -# checks that a list of integers are consequtive def contiguous(l, start, end): + """ checks that a list of integers are consequtive """ if not l: return False if l[0] != start: From 85035fb32d36a8fb944242629647e20d07e4fc73 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Nov 2015 17:00:35 -0500 Subject: [PATCH 268/569] misc docstring fixes --- obd/obd.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/obd/obd.py b/obd/obd.py index 847a8d27..71d0b918 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -135,7 +135,7 @@ def __load_commands(self): def close(self): """ - Closes the connection, and clear supported_commands + Closes the connection, and clears supported_commands """ self.supported_commands = [] @@ -147,6 +147,7 @@ def close(self): def status(self): + """ returns the OBD connection status """ if self.port is None: return OBDStatus.NOT_CONNECTED else: @@ -170,6 +171,7 @@ def protocol_name(self): def get_port_name(self): + # TODO: deprecated, remove later print("OBD.get_port_name() is deprecated, use OBD.port_name() instead") return self.port_name() From 445a086e460d0871165ac967480a886eb48ce840 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 1 Nov 2015 17:31:49 -0500 Subject: [PATCH 269/569] proof of concept for new decoders --- obd/OBDCommand.py | 38 ++++++++++++++------------------------ obd/decoders.py | 14 ++++++++------ obd/utils.py | 29 ++++++++++++++++------------- 3 files changed, 38 insertions(+), 43 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 2a617799..86fce4b5 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -84,39 +84,29 @@ def __call__(self, messages): for_us = lambda m: self.ecu & m.ecu > 0 messages = list(filter(for_us, messages)) + # guarantee data size for the decoder + for m in messages: + self.__constrain_message_data(m) # create the response object with the raw data recieved # and reference to original command r = OBDResponse(self, messages) - - - - - # combine the bytes back into a hex string - # TODO: rewrite decoders to handle raw byte arrays - _data = "" - - # filter for applicable messages - for message in messages: - for b in message.data: - h = hex(b)[2:].upper() - h = "0" + h if len(h) < 2 else h - _data += h - - # constrain number of bytes in response - if (self.bytes > 0): # zero bytes means flexible response - _data = constrainHex(_data, self.bytes) - - # decoded value into the response object - d = self.decode(_data) - r.value = d[0] - r.unit = d[1] + if messages: + r.value, r.unit = self.decode(messages) return r + def __constrain_message_data(self, message): """ pads or chops the data field to the size specified by this command """ - pass + if self.bytes > 0: + if len(message.data) > self.bytes: + # chop off the right side + message.data = message.data[:self.bytes] + else: + # pad the right with zeros + message.data += ([0] * (self.bytes - len(message.data))) + def __str__(self): return "%s: %s" % (self.command, self.desc) diff --git a/obd/decoders.py b/obd/decoders.py index e4d74408..fe480319 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -54,8 +54,9 @@ def noop(_hex): return (_hex, Unit.NONE) # hex in, bitstring out -def pid(_hex): - v = bitstring(_hex, len(_hex) * 4) +def pid(messages): + d = messages[0].data + v = bytes_to_bits(d) return (v, Unit.NONE) ''' @@ -153,9 +154,9 @@ def evap_pressure_alt(_hex): return (v, Unit.PA) # 0 to 16,383.75 RPM -def rpm(_hex): - v = unhex(_hex) - v = v / 4.0 +def rpm(messages): + d = messages[0].data + v = bytes_to_int(d) / 4.0 return (v, Unit.RPM) # 0 to 255 KPH @@ -217,7 +218,8 @@ def fuel_rate(_hex): def status(_hex): - bits = bitstring(_hex, 32) + d = messages[0].data + bits = bytes_to_bits(d) output = Status() output.MIL = bitToBool(bits[0]) diff --git a/obd/utils.py b/obd/utils.py index 16b32c36..05890a15 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -63,6 +63,22 @@ def unhex(_hex): def unbin(_bin): return int(_bin, 2) +def bytes_to_int(bs): + """ converts a big-endian byte array into a single integer """ + v = 0 + p = 0 + for b in reversed(bs): + v += b * (2**p) + p += 8 + return v + +def bytes_to_bits(bs): + bits = "" + for b in bs: + v = bin(b)[2:] + bits += ("0" * (8 - len(v))) + v # pad it with zeros + return bits + def ascii_to_bytes(a): """ converts a string of hex to an array of integer byte values """ return [ unhex(a[i:i+2]) for i in range(0, len(a), 2) ] @@ -85,19 +101,6 @@ def twos_comp(val, num_bits): def isHex(_hex): return all([c in string.hexdigits for c in _hex]) -def constrainHex(_hex, b): - """pads or chops hex to the requested number of bytes""" - diff = (b * 2) - len(_hex) # length discrepency in number of hex digits - - if diff > 0: - debug("Receieved less data than expected, trying to parse anyways...") - _hex += ('0' * diff) # pad the right side with zeros - elif diff < 0: - debug("Receieved more data than expected, trying to parse anyways...") - _hex = _hex[:diff] # chop off the right side to fit - - return _hex - def contiguous(l, start, end): """ checks that a list of integers are consequtive """ if not l: From 6e5b0746a9ef730c53de20422fa2aa0b08289d82 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 3 Nov 2015 20:35:22 -0500 Subject: [PATCH 270/569] added better protocol examples in comments --- obd/protocols/protocol_can.py | 50 ++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index ea8d6118..10795b57 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -55,6 +55,11 @@ def parse_frame(self, frame): # pad 11-bit CAN headers out to 32 bits for consistency, # since ELM already does this for 29-bit CAN headers + + # 7 E8 06 41 00 BE 7F B8 13 + # to: + # 00 00 07 E8 06 41 00 BE 7F B8 13 + if self.id_bits == 11: raw = "00000" + raw @@ -87,14 +92,15 @@ def parse_frame(self, frame): frame.rx_id = raw_bytes[2] # 0x33 = broadcast (functional) frame.tx_id = raw_bytes[3] # 0xF1 = tester ID - # Ex. + # extract the frame data # [ Frame ] # 00 00 07 E8 06 41 00 BE 7F B8 13 - frame.data = raw_bytes[4:] # read PCI byte (always first byte in the data section) + # v + # 00 00 07 E8 06 41 00 BE 7F B8 13 frame.type = frame.data[0] & 0xF0 if frame.type not in [self.FRAME_TYPE_SF, self.FRAME_TYPE_FF, @@ -102,11 +108,16 @@ def parse_frame(self, frame): debug("Dropping frame carrying unknown PCI frame type") return False + if frame.type == self.FRAME_TYPE_SF: # single frames have 4 bit length codes + # v + # 00 00 07 E8 06 41 00 BE 7F B8 13 frame.data_len = frame.data[0] & 0x0F elif frame.type == self.FRAME_TYPE_FF: # First frames have 12 bit length codes + # v + # 00 00 07 E8 06 41 00 BE 7F B8 13 frame.data_len = (frame.data[0] & 0x0F) << 8 frame.data_len += frame.data[1] elif frame.type == self.FRAME_TYPE_CF: @@ -128,6 +139,9 @@ def parse_message(self, message): return False # extract data, ignore PCI byte and anything after the marked length + # [ Frame ] + # [ Data ] + # 00 00 07 E8 06 41 00 BE 7F B8 13 xx xx xx xx, anything else is ignored message.data = frame.data[1:1+frame.data_len] else: @@ -174,13 +188,33 @@ def parse_message(self, message): # sort the sequence indices cf = sorted(cf, key=lambda f: f.seq_index) - # check contiguity + # check contiguity, and that we aren't missing any frames indices = [f.seq_index for f in cf] if not contiguous(indices, 1, len(cf)): debug("Recieved multiline response with missing frames") return False + # first frame: + # [ Frame ] + # [PCI] <-- first frame has a 2 byte PCI + # [L ] [ Data ] L = length of message in bytes + # 00 00 07 E8 10 13 49 04 01 35 36 30 + + + # consecutive frame: + # [ Frame ] + # [] <-- consecutive frames have a 1 byte PCI + # N [ Data ] N = current frame number (rolls over to 0 after F) + # 00 00 07 E8 21 32 38 39 34 39 41 43 + # 00 00 07 E8 22 00 00 00 00 00 00 31 + + + # original data: + # [ specified message length (from first-frame) ] + # 49 04 01 35 36 30 32 38 39 34 39 41 43 00 00 00 00 00 00 31 + + # on the first frame, skip PCI byte AND length code message.data += ff[0].data[2:] @@ -193,6 +227,8 @@ def parse_message(self, message): mode = message.data[0] if mode == 0x43: + # TODO: confirm this logic. I don't have any raw test data for it yet + # fetch the DTC count, and use it as a length code num_dtc_bytes = message.data[1] * 2 @@ -201,6 +237,14 @@ def parse_message(self, message): else: # handles cases when there is both a Mode and PID byte + # + # single line response: + # [ Data ] + # 00 00 07 E8 06 41 00 BE 7F B8 13 + # + # OR, the data from a multiline response: + # [ Data ] + # 49 04 01 35 36 30 32 38 39 34 39 41 43 00 00 00 00 00 00 message.data = message.data[2:] return True From 9e0f34bb40363c8d55abdcdf71a1fcd06eaea0cb Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 3 Nov 2015 20:39:59 -0500 Subject: [PATCH 271/569] removed redundant decoder todo --- obd/commands.py | 8 ++++---- obd/decoders.py | 4 ---- tests/test_OBD.py | 4 ++-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index ae200e44..0a0d90da 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -117,10 +117,10 @@ # name description cmd bytes decoder ECU fast OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "0140", 4, pid, ECU.ENGINE, True), - OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "0141", 4, todo, ECU.ENGINE, True), - OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "0142", 2, todo, ECU.ENGINE, True), - OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "0143", 2, todo, ECU.ENGINE, True), - OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "0144", 2, todo, ECU.ENGINE, True), + OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "0141", 4, noop, ECU.ENGINE, True), + OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "0142", 2, noop, ECU.ENGINE, True), + OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "0143", 2, noop, ECU.ENGINE, True), + OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "0144", 2, noop, ECU.ENGINE, True), OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , "0145", 1, percent, ECU.ENGINE, True), OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , "0146", 1, temp, ECU.ENGINE, True), OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , "0147", 1, percent, ECU.ENGINE, True), diff --git a/obd/decoders.py b/obd/decoders.py index fe480319..a8974ef3 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -45,10 +45,6 @@ def (_hex): ''' -# todo -def todo(_hex): - return (_hex, Unit.NONE) - # hex in, hex out def noop(_hex): return (_hex, Unit.NONE) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 7d81cf76..6fa4a5e4 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -1,6 +1,6 @@ import obd -from obd.utils import SerialStatus +from obd.utils import OBDStatus from obd.OBDResponse import OBDResponse from obd.OBDCommand import OBDCommand from obd.decoders import noop @@ -32,7 +32,7 @@ def write(cmd): o.is_connected = lambda *args: True o.port.is_connected = lambda *args: True - o.port._ELM327__status = SerialStatus.CAR_CONNECTED + o.port._ELM327__status = OBDStatus.CAR_CONNECTED o.port._ELM327__protocol = SAE_J1850_PWM([]) o.port._ELM327__primary_ecu = 0x10 o.port._ELM327__write = write From 329f2f3bba22e8dbb1eccea7d38efe4752deddad Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 3 Nov 2015 21:13:37 -0500 Subject: [PATCH 272/569] converted most decoders to accept list of messages --- obd/decoders.py | 117 +++++++++++++++++++++++++++++------------------- 1 file changed, 70 insertions(+), 47 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index a8974ef3..8d2c8521 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -60,31 +60,36 @@ def pid(messages): Return Value object with value and units ''' -def count(_hex): - v = unhex(_hex) +def count(messages): + d = messages[0].data + v = bytes_to_int(d) return (v, Unit.COUNT) # 0 to 100 % -def percent(_hex): - v = unhex(_hex[0:2]) +def percent(messages): + d = messages[0].data + v = d[0] v = v * 100.0 / 255.0 return (v, Unit.PERCENT) # -100 to 100 % -def percent_centered(_hex): - v = unhex(_hex[0:2]) +def percent_centered(messages): + d = messages[0].data + v = d[0] v = (v - 128) * 100.0 / 128.0 return (v, Unit.PERCENT) # -40 to 215 C -def temp(_hex): - v = unhex(_hex) +def temp(messages): + d = messages[0].data + v = bytes_to_int(d) v = v - 40 return (v, Unit.C) # -40 to 6513.5 C -def catalyst_temp(_hex): - v = unhex(_hex) +def catalyst_temp(messages): + d = messages[0].data + v = bytes_to_int(d) v = (v / 10.0) - 40 return (v, Unit.C) @@ -95,57 +100,66 @@ def current_centered(_hex): return (v, Unit.MA) # 0 to 1.275 volts -def sensor_voltage(_hex): - v = unhex(_hex[0:2]) +def sensor_voltage(messages): + d = messages[0].data + v = d[0] v = v / 200.0 return (v, Unit.VOLT) # 0 to 8 volts -def sensor_voltage_big(_hex): - v = unhex(_hex[4:8]) +def sensor_voltage_big(messages): + d = messages[0].data + v = bytes_to_int(d[2:4]) v = (v * 8.0) / 65535 return (v, Unit.VOLT) # 0 to 765 kPa -def fuel_pressure(_hex): - v = unhex(_hex) +def fuel_pressure(messages): + d = messages[0].data + v = d[0] v = v * 3 return (v, Unit.KPA) # 0 to 255 kPa -def pressure(_hex): - v = unhex(_hex) +def pressure(messages): + d = messages[0].data + v = d[0] return (v, Unit.KPA) # 0 to 5177 kPa -def fuel_pres_vac(_hex): - v = unhex(_hex) +def fuel_pres_vac(messages): + d = messages[0].data + v = bytes_to_int(d) v = v * 0.079 return (v, Unit.KPA) # 0 to 655,350 kPa -def fuel_pres_direct(_hex): - v = unhex(_hex) +def fuel_pres_direct(messages): + d = messages[0].data + v = bytes_to_int(d) v = v * 10 return (v, Unit.KPA) # -8192 to 8192 Pa -def evap_pressure(_hex): +def evap_pressure(messages): # decode the twos complement - a = twos_comp(unhex(_hex[0:2]), 8) - b = twos_comp(unhex(_hex[2:4]), 8) + d = messages[0].data + a = twos_comp(unhex(d[0]), 8) + b = twos_comp(unhex(d[1]), 8) v = ((a * 256.0) + b) / 4.0 return (v, Unit.PA) # 0 to 327.675 kPa -def abs_evap_pressure(_hex): - v = unhex(_hex) +def abs_evap_pressure(messages): + d = messages[0].data + v = bytes_to_int(d) v = v / 200.0 return (v, Unit.KPA) # -32767 to 32768 Pa -def evap_pressure_alt(_hex): - v = unhex(_hex) +def evap_pressure_alt(messages): + d = messages[0].data + v = bytes_to_int(d) v = v - 32767 return (v, Unit.PA) @@ -156,52 +170,61 @@ def rpm(messages): return (v, Unit.RPM) # 0 to 255 KPH -def speed(_hex): - v = unhex(_hex) +def speed(messages): + d = messages[0].data + v = bytes_to_int(d) return (v, Unit.KPH) # -64 to 63.5 degrees -def timing_advance(_hex): - v = unhex(_hex) +def timing_advance(messages): + d = messages[0].data + v = d[0] v = (v - 128) / 2.0 return (v, Unit.DEGREES) # -210 to 301 degrees -def inject_timing(_hex): - v = unhex(_hex) +def inject_timing(messages): + d = messages[0].data + v = bytes_to_int(d) v = (v - 26880) / 128.0 return (v, Unit.DEGREES) # 0 to 655.35 grams/sec -def maf(_hex): - v = unhex(_hex) +def maf(messages): + d = messages[0].data + v = bytes_to_int(d) v = v / 100.0 return (v, Unit.GPS) # 0 to 2550 grams/sec -def max_maf(_hex): - v = unhex(_hex[0:2]) +def max_maf(messages): + d = messages[0].data + v = d[0] v = v * 10 return (v, Unit.GPS) # 0 to 65535 seconds -def seconds(_hex): - v = unhex(_hex) +def seconds(messages): + d = messages[0].data + v = bytes_to_int(d) return (v, Unit.SEC) # 0 to 65535 minutes -def minutes(_hex): - v = unhex(_hex) +def minutes(messages): + d = messages[0].data + v = bytes_to_int(d) return (v, Unit.MIN) # 0 to 65535 km -def distance(_hex): - v = unhex(_hex) +def distance(messages): + d = messages[0].data + v = bytes_to_int(d) return (v, Unit.KM) # 0 to 3212 Liters/hour -def fuel_rate(_hex): - v = unhex(_hex) +def fuel_rate(messages): + d = messages[0].data + v = bytes_to_int(d) v = v * 0.05 return (v, Unit.LPH) From f3aa46730dbf2de58243cc953443316959efa9f0 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 3 Nov 2015 21:19:54 -0500 Subject: [PATCH 273/569] handle CAN length fields, fixed protocol docs --- obd/protocols/README.md | 2 +- obd/protocols/protocol_can.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/obd/protocols/README.md b/obd/protocols/README.md index 5d7d5a8b..7f6a250f 100644 --- a/obd/protocols/README.md +++ b/obd/protocols/README.md @@ -3,7 +3,7 @@ Notes Each protocol object is callable, and accepts a list of raw input strings, and returns a list of parsed `Message` objects. The `data` field will contain a list of integers, corresponding to all relevant data returned by the command. -*Note: `Message.data` does not refer to the full data field of a message, but rather a subset of this field. Things like Mode/PID/PCI bytes are removed. However, `Frame.data_bytes` DOES include the full data field (per-spec), for each frame.* +*Note: `Message.data` does not refer to the full data field of a message. Things like PCI/Mode/PID bytes are removed. If you want to see these fields, use `Frame.data` for the full (per-spec) data field.* For example, these are the resultant `Message.data` fields for some single frame messages: diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 10795b57..cbcefbe2 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -216,12 +216,15 @@ def parse_message(self, message): # on the first frame, skip PCI byte AND length code - message.data += ff[0].data[2:] + message.data = ff[0].data[2:] # now that they're in order, load/accumulate the data from each CF frame for f in cf: message.data += f.data[1:] # chop off the PCI byte + # chop to the correct size (as specified in the first frame) + message.data = message.data[:ff[0].data_len] + # chop off the Mode/PID bytes based on the mode number mode = message.data[0] From 5adee8b232ec1b5075a8a3f4b4e7e6795f5724fd Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 3 Nov 2015 21:26:29 -0500 Subject: [PATCH 274/569] converted more decoders to use messages --- obd/decoders.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 8d2c8521..35b38fd1 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -236,7 +236,7 @@ def fuel_rate(messages): -def status(_hex): +def status(messages): d = messages[0].data bits = bytes_to_bits(d) @@ -283,8 +283,9 @@ def status(_hex): -def fuel_status(_hex): - v = unhex(_hex[0:2]) # todo, support second fuel system +def fuel_status(messages): + d = messages[0].data + v = d[0] # todo, support second fuel system if v <= 0: debug("Invalid fuel status response (v <= 0)", True) @@ -305,8 +306,9 @@ def fuel_status(_hex): return (FUEL_STATUS[i], Unit.NONE) -def air_status(_hex): - v = unhex(_hex) +def air_status(messages): + d = messages[0].data + v = d[0] if v <= 0: debug("Invalid air status response (v <= 0)", True) @@ -328,7 +330,8 @@ def air_status(_hex): def obd_compliance(_hex): - i = unhex(_hex) + d = messages[0].data + i = d[0] v = "Error: Unknown OBD compliance response" @@ -339,7 +342,8 @@ def obd_compliance(_hex): def fuel_type(_hex): - i = unhex(_hex) + d = messages[0].data + i = d[0] # todo, support second fuel system v = "Error: Unknown fuel type response" From 674dd22fb17279b1f9461cffa0a1f76c4effcf3a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 4 Nov 2015 14:16:07 -0500 Subject: [PATCH 275/569] implemented decoder for ELM voltage --- obd/async.py | 4 ++-- obd/commands.py | 2 +- obd/decoders.py | 12 ++++++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/obd/async.py b/obd/async.py index 149d9947..99783ec9 100644 --- a/obd/async.py +++ b/obd/async.py @@ -41,8 +41,8 @@ class Async(OBD): Specialized for asynchronous value reporting. """ - def __init__(self, portstr=None, baudrate=38400): - super(Async, self).__init__(portstr, baudrate) + def __init__(self, portstr=None, baudrate=38400, protocol=None, fast=True): + super(Async, self).__init__(portstr, baudrate, protocol, fast) self.__commands = {} # key = OBDCommand, value = Response self.__callbacks = {} # key = OBDCommand, value = list of Functions self.__thread = None diff --git a/obd/commands.py b/obd/commands.py index 0a0d90da..483bb779 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -178,7 +178,7 @@ __misc__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("VOLTAGE" , "Voltage detected by OBD-II adapter" , "ATRV", 0, noop, ECU.UNKNOWN, False, True), + OBDCommand("VOLTAGE" , "Voltage detected by OBD-II adapter" , "ATRV", 0, elm_voltage, ECU.UNKNOWN, False, True), ] diff --git a/obd/decoders.py b/obd/decoders.py index 35b38fd1..483a26a7 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -46,8 +46,8 @@ def (_hex): # hex in, hex out -def noop(_hex): - return (_hex, Unit.NONE) +def noop(messages): + return (None, Unit.NONE) # hex in, bitstring out def pid(messages): @@ -229,6 +229,14 @@ def fuel_rate(messages): return (v, Unit.LPH) +def elm_voltage(messages): + # doesn't register as a normal OBD response, + # so access the raw frame data + v = messages[0].frames[0].raw + v = float(v) + return (v, Unit.VOLT) + + ''' Special decoders Return objects, lists, etc From d559a2baa75eebb03597554be6ed3626f03a0334 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 4 Nov 2015 16:59:29 -0500 Subject: [PATCH 276/569] enabling debug must come before connection code --- docs/Troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index cba73af2..324d9366 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -1,7 +1,7 @@ # Debug Output -If python-OBD is not working properly, the first thing you should do is enable debug output. The following line enables console printing: +If python-OBD is not working properly, the first thing you should do is enable debug output. Add the following line before your connection code to print all of the debug information to your console: ```python obd.debug.console = True From e7294d9d1afe4977eeb190ad5a69ce4a274cb6f9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 6 Nov 2015 20:49:30 -0500 Subject: [PATCH 277/569] ignore ELM messages while populating the ECU map --- obd/protocols/protocol.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index c206c629..d62d00fb 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -80,6 +80,10 @@ def tx_id(self): else: return self.frames[0].tx_id + def parsed(self): + """ boolean for whether this message was successfully parsed """ + return bool(self.data) + def __eq__(self, other): if isinstance(other, Message): for attr in ["frames", "ecu", "data"]: @@ -221,6 +225,11 @@ def populate_ecu_map(self, messages): This is mostly concerned with finding the engine. """ + # filter out messages that don't contain any data + # this will prevent ELM responses from being mapped to ECUs + messages = filter(lambda m: m.parsed(), messages) + + # populate the map if len(messages) == 0: pass elif len(messages) == 1: From 2dc2437dbd2efbea325ea0ced4251f795bb95c07 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 6 Nov 2015 21:26:04 -0500 Subject: [PATCH 278/569] converted DTC decoders to work with messages --- obd/decoders.py | 47 ++++++++++++++++++++++++++++------------------- obd/utils.py | 7 +++++++ 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index fe480319..b7ad4ff6 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -330,38 +330,47 @@ def fuel_type(_hex): return (v, Unit.NONE) -# converts 2 bytes of hex into a DTC code -def single_dtc(_hex): +def single_dtc(_bytes): + """ converts 2 bytes into a DTC code """ - if len(_hex) != 4: + # check validity (also ignores padding that the ELM returns) + if (len(_bytes) != 2) or (_bytes == (0,0)): return None - if _hex == "0000": - return None - - bits = bitstring(_hex[0], 4) + # BYTES: (16, 35 ) + # HEX: 4 1 2 3 + # BIN: 01000001 00100011 + # [][][ in hex ] + # | / / + # DTC: C0123 - dtc = "" - dtc += ['P', 'C', 'B', 'U'][unbin(bits[0:2])] - dtc += str(unbin(bits[2:4])) - dtc += _hex[1:4] + dtc = ['P', 'C', 'B', 'U'][ _bytes[0] >> 6 ] # the last 2 bits of the first byte + dtc += str( (_bytes[0] >> 4) & 0b0011 ) # the next pair of 2 bits. Mask off the bits we read above + dtc += bytes_to_hex(_bytes)[1:4] return dtc -# converts a frame of 2-byte DTCs into a list of DTCs -# example input = "010480034123" -# [ ][ ][ ] -def dtc(_hex): +def dtc(messages): + """ converts a frame of 2-byte DTCs into a list of DTCs """ codes = [] - for n in range(0, len(_hex), 4): - dtc = single_dtc(_hex[n:n+4]) + d = [] + for message in messages: + d += message.data - if dtc is not None: + print(bytes_to_hex(d)) + # look at data in pairs of bytes + for n in range(0, len(d), 2): + + # parse the code + dtc = single_dtc( (d[n], d[n+1]) ) + + if dtc is not None: # pull a description if we have one - desc = "Unknown error code" if dtc in DTC: desc = DTC[dtc] + else: + desc = "Unknown error code" codes.append( (dtc, desc) ) diff --git a/obd/utils.py b/obd/utils.py index 05890a15..54d620da 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -79,6 +79,13 @@ def bytes_to_bits(bs): bits += ("0" * (8 - len(v))) + v # pad it with zeros return bits +def bytes_to_hex(bs): + h = "" + for b in bs: + bh = hex(b)[2:] + h += ("0" * (2 - len(bh))) + bh + return h + def ascii_to_bytes(a): """ converts a string of hex to an array of integer byte values """ return [ unhex(a[i:i+2]) for i in range(0, len(a), 2) ] From d020ab6322ad7dd6202932860986f7c91457ed0c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 6 Nov 2015 21:32:26 -0500 Subject: [PATCH 279/569] using list comprehension in order to get list length --- obd/protocols/protocol.py | 2 +- obd/protocols/protocol_can.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index d62d00fb..e8b0dad9 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -227,7 +227,7 @@ def populate_ecu_map(self, messages): # filter out messages that don't contain any data # this will prevent ELM responses from being mapped to ECUs - messages = filter(lambda m: m.parsed(), messages) + messages = [ m for m in messages if m.parsed() ] # populate the map if len(messages) == 0: diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index cbcefbe2..9b07cbef 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -239,7 +239,7 @@ def parse_message(self, message): message.data = message.data[2:][:num_dtc_bytes] else: - # handles cases when there is both a Mode and PID byte + # skip the Mode and PID bytes # # single line response: # [ Data ] From c091439dc1abc0c56d393b800574a7bc832c5f1f Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 6 Nov 2015 22:14:02 -0500 Subject: [PATCH 280/569] allow ELM to already have echo disabled --- obd/elm327.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/obd/elm327.py b/obd/elm327.py index 6403581c..8d7519ca 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -204,7 +204,9 @@ def __isok(self, lines, expectEcho=False): if not lines: return False if expectEcho: - return len(lines) == 2 and lines[1] == 'OK' + # don't test for the echo itself + # allow the adapter to already have echo disabled + return self.__has_message(lines, 'OK') else: return len(lines) == 1 and lines[0] == 'OK' From 298612790ef071efc3a62e58f895bd6600762d77 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 6 Nov 2015 22:14:50 -0500 Subject: [PATCH 281/569] misc formatting tweaks --- obd/elm327.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 8d7519ca..974f038c 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -87,20 +87,20 @@ class ELM327: def __init__(self, portname, baudrate, protocol): """Initializes port by resetting device and gettings supported PIDs. """ - self.__status = OBDStatus.NOT_CONNECTED - self.__port = None - self.__protocol = UnknownProtocol([]) + self.__status = OBDStatus.NOT_CONNECTED + self.__port = None + self.__protocol = UnknownProtocol([]) # ------------- open port ------------- try: debug("Opening serial port '%s'" % portname) self.__port = serial.Serial(portname, \ - baudrate = baudrate, \ - parity = serial.PARITY_NONE, \ - stopbits = 1, \ - bytesize = 8, \ - timeout = 3) # seconds + baudrate = baudrate, \ + parity = serial.PARITY_NONE, \ + stopbits = 1, \ + bytesize = 8, \ + timeout = 3) # seconds debug("Serial port successfully opened on " + self.port_name()) except serial.SerialException as e: @@ -211,9 +211,9 @@ def __isok(self, lines, expectEcho=False): return len(lines) == 1 and lines[0] == 'OK' - def __has_message(self, lines, message): + def __has_message(self, lines, text): for line in lines: - if message in line: + if text in line: return True return False From aaef42bdf82eb3ba1e58f0079615c2c5a3ac1f95 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 6 Nov 2015 22:58:26 -0500 Subject: [PATCH 282/569] added protocol ID getter --- docs/Connections.md | 28 +++++++++++++++------------- obd/elm327.py | 4 ++++ obd/obd.py | 8 ++++++++ 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/docs/Connections.md b/docs/Connections.md index da6f9b0f..a9715ca6 100644 --- a/docs/Connections.md +++ b/docs/Connections.md @@ -85,21 +85,23 @@ Returns a boolean for whether a command is supported by both the car and python- --- +### protocol_id() ### protocol_name() -Returns the string name of the protocol being used by the adapter. This function does not make any serial requests. The possible values are: - -- `""` when no connection has been made -- `"SAE J1850 PWM"` -- `"SAE J1850 VPW"` -- `"AUTO, ISO 9141-2"` -- `"ISO 14230-4 (KWP 5BAUD)"` -- `"ISO 14230-4 (KWP FAST)"` -- `"ISO 15765-4 (CAN 11/500)"` -- `"ISO 15765-4 (CAN 29/500)"` -- `"ISO 15765-4 (CAN 11/250)"` -- `"ISO 15765-4 (CAN 29/250)"` -- `"SAE J1939 (CAN 29/250)"` +Both functions return string names for the protocol currently being used by the adapter. Protocol *ID's* are the short names used by your adapter, whereas protocol *names* are the human-readable versions. The `protocol_id()` function is a good way to lookup which value to pass in the `protocol` field of the OBD constructor (though, this is mainly for advanced usage). These function do not make any serial requests. When no connection has been made, these functions will return empty strings. The possible values are: + +|ID | Name | +|---|--------------------------| +| 1 | SAE J1850 PWM | +| 2 | SAE J1850 VPW | +| 3 | AUTO, ISO 9141-2 | +| 4 | ISO 14230-4 (KWP 5BAUD) | +| 5 | ISO 14230-4 (KWP FAST) | +| 6 | ISO 15765-4 (CAN 11/500) | +| 7 | ISO 15765-4 (CAN 29/500) | +| 8 | ISO 15765-4 (CAN 11/250) | +| 9 | ISO 15765-4 (CAN 29/250) | +| A | SAE J1939 (CAN 29/250) | --- diff --git a/obd/elm327.py b/obd/elm327.py index 974f038c..a9d8fcdd 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -247,6 +247,10 @@ def protocol_name(self): return self.__protocol.ELM_NAME + def protocol_id(self): + return self.__protocol.ELM_ID + + def close(self): """ Resets the device, and sets all diff --git a/obd/obd.py b/obd/obd.py index 71d0b918..2c3c7789 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -170,6 +170,14 @@ def protocol_name(self): return self.port.protocol_name() + def protocol_id(self): + """ returns the ID of the protocol being used by the ELM327 """ + if self.port is None: + return "" + else: + return self.port.protocol_id() + + def get_port_name(self): # TODO: deprecated, remove later print("OBD.get_port_name() is deprecated, use OBD.port_name() instead") From 9259f062dcb8707838b1e1f6f13f11aff6224a8b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 7 Nov 2015 22:48:59 -0500 Subject: [PATCH 283/569] decided not to publish the ecus() API just yet --- docs/Connections.md | 4 +++- obd/obd.py | 16 +++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/Connections.md b/docs/Connections.md index a9715ca6..47ebdd74 100644 --- a/docs/Connections.md +++ b/docs/Connections.md @@ -105,6 +105,8 @@ Both functions return string names for the protocol currently being used by the --- + ### close() diff --git a/obd/obd.py b/obd/obd.py index 2c3c7789..f6b531e2 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -154,12 +154,14 @@ def status(self): return self.port.status() - def ecus(self): - """ returns a list of ECUs in the vehicle """ - if self.port is None: - return [] - else: - return self.port.ecus() + # not sure how useful this would be + + # def ecus(self): + # """ returns a list of ECUs in the vehicle """ + # if self.port is None: + # return [] + # else: + # return self.port.ecus() def protocol_name(self): @@ -255,7 +257,7 @@ def __build_command_string(self, cmd): cmd_string = cmd.command if self.fast and cmd.fast: - cmd_string += str(len(self.ecus())) + cmd_string += str(len(self.port.ecus())) # if we sent this last time, just send if self.fast and (cmd_string == self.__last_command): From 9c626f669be82775a3b714b9832ab470f30059e3 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 23 Jan 2016 23:57:23 -0500 Subject: [PATCH 284/569] updated base protocol tests for new system --- tests/test_protocol.py | 67 ++++++++++++++---------------------------- 1 file changed, 22 insertions(+), 45 deletions(-) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index ffe89698..624c4bd4 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -20,30 +20,34 @@ def test_ECU(): def test_frame(): # constructor - f = Frame("asdf") - assert f.raw == "asdf", "Frame failed to accept raw data as __init__ argument" - assert f.priority == None - assert f.addr_mode == None - assert f.rx_id == None - assert f.tx_id == None - assert f.type == None - assert f.seq_index == 0 - assert f.data_len == None + frame = Frame("asdf") + assert frame.raw == "asdf", "Frame failed to accept raw data as __init__ argument" + assert frame.priority == None + assert frame.addr_mode == None + assert frame.rx_id == None + assert frame.tx_id == None + assert frame.type == None + assert frame.seq_index == 0 + assert frame.data_len == None def test_message(): # constructor - f = Frame("") - f.tx_id = 42 - R = ["asdf"] - F = [f] - m = Message(R, F) + frame = Frame("raw input from OBD tool") + frame.tx_id = 42 + + frames = [frame] + + # a message is simply a special container for a bunch of frames + message = Message(frames) + + assert message.frames == frames + assert message.ecu == ECU.UNKNOWN + assert message.tx_id == 42 # this is dynamically read from the first frame + + assert Message([]).tx_id == None # if no frames are given, then we can't report a tx_id - assert m.raw == R - assert m.frames == F - assert m.tx_id == 42 - assert m.ecu == ECU.UNKNOWN def test_populate_ecu_map(): @@ -64,30 +68,3 @@ def test_populate_ecu_map(): # if no messages were received, then the map is empty p = SAE_J1850_PWM([]) assert len(p.ecu_map) == 0 - - -def test_call_filtering(): - - # test the basic frame construction - p = UnknownProtocol([]) - - f1 = "48 6B 12 41 00 BE 1F B8 11 AA" - f2 = "48 6B 14 41 00 00 00 B8 11 AA" - raw = [f1, f2] - m = p(raw) - assert len(m) == 1 - assert len(m[0].frames) == 2 - assert m[0].raw == raw - assert m[0].frames[0].raw == f1.replace(' ', '') - assert m[0].frames[1].raw == f2.replace(' ', '') - - - # test invalid hex dropping - p = UnknownProtocol([]) - - raw = ["not hex", f2] - m = p(raw) - assert len(m) == 1 - assert len(m[0].frames) == 1 - assert m[0].raw == raw - assert m[0].frames[0].raw == f2.replace(' ', '') From 98836109abfa470bedde55ef1399bcc5bb0f9928 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jan 2016 00:32:52 -0500 Subject: [PATCH 285/569] protocol parsers pass non-hex messages now --- tests/test_protocol_legacy.py | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/tests/test_protocol_legacy.py b/tests/test_protocol_legacy.py index 71cfb8a4..5787dc28 100644 --- a/tests/test_protocol_legacy.py +++ b/tests/test_protocol_legacy.py @@ -47,23 +47,41 @@ def test_single_frame(): def test_hex_straining(): + """ + If non-hex values are sent, they should be marked as ECU.UNKNOWN + """ + for protocol in LEGACY_PROTOCOLS: p = protocol([]) + # single non-hex message + r = p(["12.8 Volts"]) + assert len(r) == 1 + assert r[0].ecu == ECU.UNKNOWN + assert len(r[0].frames) == 1 - r = p(["NO DATA"]) - assert len(r) == 0 - r = p(["TOTALLY NOT HEX"]) - assert len(r) == 0 - - r = p(["NO DATA", "NO DATA"]) - assert len(r) == 0 + # multiple non-hex message + r = p(["12.8 Volts", "NO DATA"]) + assert len(r) == 2 + for m in r: + assert m.ecu == ECU.UNKNOWN + assert len(m.frames) == 1 + + # mixed hex and non-hex r = p(["NO DATA", "48 6B 10 41 00 00 01 02 03 FF"]) - assert len(r) == 1 + assert len(r) == 2 + + # first message should be the valid, parsable hex message + # NOTE: the parser happens to process the valid one's first check_message(r[0], 1, 0x10, list(range(4))) + # second message: invalid, non-parsable non-hex + assert r[1].ecu == ECU.UNKNOWN + assert len(r[1].frames) == 1 + assert len(r[1].data) == 0 # no data + def test_multi_ecu(): From aa67843f28033cd8c1b52c21e37ee834f876fd58 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jan 2016 00:38:33 -0500 Subject: [PATCH 286/569] split the giant multiline test into several smaller tests --- tests/test_protocol_legacy.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/tests/test_protocol_legacy.py b/tests/test_protocol_legacy.py index 5787dc28..49a78b95 100644 --- a/tests/test_protocol_legacy.py +++ b/tests/test_protocol_legacy.py @@ -16,9 +16,9 @@ ] -def check_message(m, num_frames, tx_id, data): +def check_message(m, n_frames, tx_id, data): """ generic test for correct message values """ - assert len(m.frames) == num_frames + assert len(m.frames) == n_frames assert m.tx_id == tx_id assert m.data == data @@ -109,10 +109,14 @@ def test_multi_ecu(): def test_multi_line(): + """ + Tests that valid multiline messages are recombined into single + messages. + """ + for protocol in LEGACY_PROTOCOLS: p = protocol([]) - test_case = [ "48 6B 10 49 02 01 00 01 02 03 FF", "48 6B 10 49 02 02 04 05 06 07 FF", @@ -134,8 +138,16 @@ def test_multi_line(): check_message(r[0], len(test_case), 0x10, correct_data) - # missing frames in a multi-frame message should drop the message - # (tests the contiguity check, and data length byte) + +def test_multi_line_missing_frames(): + """ + Missing frames in a multi-frame message should drop the message. + Tests the contiguity check, and data length byte + """ + + for protocol in LEGACY_PROTOCOLS: + p = protocol([]) + test_case = [ "48 6B 10 49 02 01 00 01 02 03 FF", @@ -151,7 +163,16 @@ def test_multi_line(): assert len(r) == 0 - # MODE 03 COMMANDS (GET_DTC) RETURN NO PID BYTE +def test_multi_line_mode_03(): + """ + Tests the special handling of mode 3 commands. + Namely, Mode 03 commands (GET_DTC) return no PID byte. + When frames are combined, the parser should account for this. + """ + + for protocol in LEGACY_PROTOCOLS: + p = protocol([]) + test_case = [ "48 6B 10 43 00 01 02 03 04 05 FF", From c91530030a8f0f88a963b5dbc68b2f42167b8f82 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jan 2016 00:41:26 -0500 Subject: [PATCH 287/569] convert tabs to spaces --- tests/test_OBD.py | 170 ++++++++++++------------ tests/test_OBDCommand.py | 90 ++++++------- tests/test_commands.py | 106 +++++++-------- tests/test_decoders.py | 188 +++++++++++++------------- tests/test_protocol_can.py | 164 +++++++++++------------ tests/test_protocol_legacy.py | 240 +++++++++++++++++----------------- 6 files changed, 479 insertions(+), 479 deletions(-) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 6fa4a5e4..f9cfc45d 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -8,96 +8,96 @@ def test_is_connected(): - o = obd.OBD("/dev/null") - assert not o.is_connected() + o = obd.OBD("/dev/null") + assert not o.is_connected() - # todo + # todo """ # TODO: rewrite for new protocol architecture def test_query(): - # we don't need an actual serial connection - o = obd.OBD("/dev/null") - # forge our own command, to control the output - cmd = OBDCommand("TEST", "Test command", "0123", 2, noop, False) - - # forge IO from the car by overwriting the read/write functions - - # buffers - toCar = [""] # needs to be inside mutable object to allow assignment in closure - fromCar = "" - - def write(cmd): - toCar[0] = cmd - - o.is_connected = lambda *args: True - o.port.is_connected = lambda *args: True - o.port._ELM327__status = OBDStatus.CAR_CONNECTED - o.port._ELM327__protocol = SAE_J1850_PWM([]) - o.port._ELM327__primary_ecu = 0x10 - o.port._ELM327__write = write - o.port._ELM327__read = lambda *args: fromCar - - # make sure unsupported commands don't write ------------------------------ - fromCar = ["48 6B 10 41 23 AB CD 10"] - r = o.query(cmd) - assert toCar[0] == "" - assert r.is_null() - - # a correct command transaction ------------------------------------------- - fromCar = ["48 6B 10 41 23 AB CD 10"] # preset the response - r = o.query(cmd, force=True) # run - assert toCar[0] == "0123" # verify that the command was sent correctly - assert not r.is_null() - assert r.value == "ABCD" # verify that the response was parsed correctly - - # response of greater length ---------------------------------------------- - fromCar = ["48 6B 10 41 23 AB CD EF 10"] - r = o.query(cmd, force=True) - assert toCar[0] == "0123" - assert r.value == "ABCD" - - # response of lesser length ----------------------------------------------- - fromCar = ["48 6B 10 41 23 AB 10"] - r = o.query(cmd, force=True) - assert toCar[0] == "0123" - assert r.value == "AB00" - - # NO DATA response -------------------------------------------------------- - fromCar = ["NO DATA"] - r = o.query(cmd, force=True) - assert r.is_null() - - # malformed response ------------------------------------------------------ - fromCar = ["totaly not hex!@#$"] - r = o.query(cmd, force=True) - assert r.is_null() - - # no response ------------------------------------------------------------- - fromCar = [""] - r = o.query(cmd, force=True) - assert r.is_null() - - # reject responses from other ECUs --------------------------------------- - fromCar = ["48 6B 12 41 23 AB CD 10"] - r = o.query(cmd, force=True) - assert toCar[0] == "0123" - assert r.is_null() - - # filter for primary ECU -------------------------------------------------- - fromCar = ["48 6B 12 41 23 AB CD 10", "48 6B 10 41 23 AB CD 10"] - r = o.query(cmd, force=True) - assert toCar[0] == "0123" - assert r.value == "ABCD" - - ''' - # ignore multiline responses ---------------------------------------------- - fromCar = ["48 6B 10 41 23 AB CD 10", "48 6B 10 41 23 AB CD 10"] - r = o.query(cmd, force=True) - assert toCar[0] == "0123" - assert r.is_null() - ''' + # we don't need an actual serial connection + o = obd.OBD("/dev/null") + # forge our own command, to control the output + cmd = OBDCommand("TEST", "Test command", "0123", 2, noop, False) + + # forge IO from the car by overwriting the read/write functions + + # buffers + toCar = [""] # needs to be inside mutable object to allow assignment in closure + fromCar = "" + + def write(cmd): + toCar[0] = cmd + + o.is_connected = lambda *args: True + o.port.is_connected = lambda *args: True + o.port._ELM327__status = OBDStatus.CAR_CONNECTED + o.port._ELM327__protocol = SAE_J1850_PWM([]) + o.port._ELM327__primary_ecu = 0x10 + o.port._ELM327__write = write + o.port._ELM327__read = lambda *args: fromCar + + # make sure unsupported commands don't write ------------------------------ + fromCar = ["48 6B 10 41 23 AB CD 10"] + r = o.query(cmd) + assert toCar[0] == "" + assert r.is_null() + + # a correct command transaction ------------------------------------------- + fromCar = ["48 6B 10 41 23 AB CD 10"] # preset the response + r = o.query(cmd, force=True) # run + assert toCar[0] == "0123" # verify that the command was sent correctly + assert not r.is_null() + assert r.value == "ABCD" # verify that the response was parsed correctly + + # response of greater length ---------------------------------------------- + fromCar = ["48 6B 10 41 23 AB CD EF 10"] + r = o.query(cmd, force=True) + assert toCar[0] == "0123" + assert r.value == "ABCD" + + # response of lesser length ----------------------------------------------- + fromCar = ["48 6B 10 41 23 AB 10"] + r = o.query(cmd, force=True) + assert toCar[0] == "0123" + assert r.value == "AB00" + + # NO DATA response -------------------------------------------------------- + fromCar = ["NO DATA"] + r = o.query(cmd, force=True) + assert r.is_null() + + # malformed response ------------------------------------------------------ + fromCar = ["totaly not hex!@#$"] + r = o.query(cmd, force=True) + assert r.is_null() + + # no response ------------------------------------------------------------- + fromCar = [""] + r = o.query(cmd, force=True) + assert r.is_null() + + # reject responses from other ECUs --------------------------------------- + fromCar = ["48 6B 12 41 23 AB CD 10"] + r = o.query(cmd, force=True) + assert toCar[0] == "0123" + assert r.is_null() + + # filter for primary ECU -------------------------------------------------- + fromCar = ["48 6B 12 41 23 AB CD 10", "48 6B 10 41 23 AB CD 10"] + r = o.query(cmd, force=True) + assert toCar[0] == "0123" + assert r.value == "ABCD" + + ''' + # ignore multiline responses ---------------------------------------------- + fromCar = ["48 6B 10 41 23 AB CD 10", "48 6B 10 41 23 AB CD 10"] + r = o.query(cmd, force=True) + assert toCar[0] == "0123" + assert r.is_null() + ''' """ def test_load_commands(): - pass + pass diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index 30280dca..480a55fd 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -5,68 +5,68 @@ def test_constructor(): - # name description mode cmd bytes decoder - cmd = OBDCommand("Test", "example OBD command", "0123", 2, noop, ECU.ENGINE) - assert cmd.name == "Test" - assert cmd.desc == "example OBD command" - assert cmd.command == "0123" - assert cmd.bytes == 2 - assert cmd.decode == noop - assert cmd.ecu == ECU.ENGINE - assert cmd.supported == False + # name description mode cmd bytes decoder + cmd = OBDCommand("Test", "example OBD command", "0123", 2, noop, ECU.ENGINE) + assert cmd.name == "Test" + assert cmd.desc == "example OBD command" + assert cmd.command == "0123" + assert cmd.bytes == 2 + assert cmd.decode == noop + assert cmd.ecu == ECU.ENGINE + assert cmd.supported == False - assert cmd.mode_int == 1 - assert cmd.pid_int == 35 + assert cmd.mode_int == 1 + assert cmd.pid_int == 35 - cmd = OBDCommand("Test", "example OBD command", "0123", 2, noop, ECU.ENGINE, True) - assert cmd.supported == True + cmd = OBDCommand("Test", "example OBD command", "0123", 2, noop, ECU.ENGINE, True) + assert cmd.supported == True def test_clone(): - # name description mode cmd bytes decoder - cmd = OBDCommand("", "", "0123", 2, noop, ECU.ENGINE) - other = cmd.clone() + # name description mode cmd bytes decoder + cmd = OBDCommand("", "", "0123", 2, noop, ECU.ENGINE) + other = cmd.clone() - assert cmd.name == other.name - assert cmd.desc == other.desc - assert cmd.command == other.command - assert cmd.bytes == other.bytes - assert cmd.decode == other.decode - assert cmd.ecu == other.ecu - assert cmd.supported == cmd.supported + assert cmd.name == other.name + assert cmd.desc == other.desc + assert cmd.command == other.command + assert cmd.bytes == other.bytes + assert cmd.decode == other.decode + assert cmd.ecu == other.ecu + assert cmd.supported == cmd.supported def test_call(): - p = SAE_J1850_PWM(["48 6B 10 41 00 FF FF FF FF AA"]) # train the ecu_map to identify the engine - messages = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) # parse valid data into response object + p = SAE_J1850_PWM(["48 6B 10 41 00 FF FF FF FF AA"]) # train the ecu_map to identify the engine + messages = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) # parse valid data into response object - # valid response size - cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) - r = cmd(messages) - assert r.value == "BE1FB811" + # valid response size + cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) + r = cmd(messages) + assert r.value == "BE1FB811" - # response too short (pad) - cmd = OBDCommand("", "", "0123", 5, noop, ECU.ENGINE) - r = cmd(messages) - assert r.value == "BE1FB81100" + # response too short (pad) + cmd = OBDCommand("", "", "0123", 5, noop, ECU.ENGINE) + r = cmd(messages) + assert r.value == "BE1FB81100" - # response too long (clip) - cmd = OBDCommand("", "", "0123", 3, noop, ECU.ENGINE) - r = cmd(messages) - assert r.value == "BE1FB8" + # response too long (clip) + cmd = OBDCommand("", "", "0123", 3, noop, ECU.ENGINE) + r = cmd(messages) + assert r.value == "BE1FB8" def test_get_mode_int(): - cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) - assert cmd.mode_int == 0x01 + cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) + assert cmd.mode_int == 0x01 - cmd = OBDCommand("", "", "", "23", 4, noop, ECU.ENGINE) - assert cmd.mode_int == 0 + cmd = OBDCommand("", "", "", "23", 4, noop, ECU.ENGINE) + assert cmd.mode_int == 0 def test_pid_int(): - cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) - assert cmd.pid_int == 0x23 + cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) + assert cmd.pid_int == 0x23 - cmd = OBDCommand("", "", "01", 4, noop, ECU.ENGINE) - assert cmd.pid_int == 0 + cmd = OBDCommand("", "", "01", 4, noop, ECU.ENGINE) + assert cmd.pid_int == 0 diff --git a/tests/test_commands.py b/tests/test_commands.py index b621d00a..0ac0df2f 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -4,81 +4,81 @@ def test_list_integrity(): - for mode, cmds in enumerate(obd.commands.modes): - for pid, cmd in enumerate(cmds): + for mode, cmds in enumerate(obd.commands.modes): + for pid, cmd in enumerate(cmds): - assert cmd.command != "", "The Command's command string must not be null" + assert cmd.command != "", "The Command's command string must not be null" - # make sure the command tables are in mode & PID order - assert mode == cmd.mode_int, "Command is in the wrong mode list: %s" % cmd.name - assert pid == cmd.pid_int, "The index in the list must also be the PID: %s" % cmd.name + # make sure the command tables are in mode & PID order + assert mode == cmd.mode_int, "Command is in the wrong mode list: %s" % cmd.name + assert pid == cmd.pid_int, "The index in the list must also be the PID: %s" % cmd.name - # make sure all the fields are set - assert cmd.name != "", "Command names must not be null" - assert cmd.name.isupper(), "Command names must be upper case" - assert ' ' not in cmd.name, "No spaces allowed in command names" - assert cmd.desc != "", "Command description must not be null" - assert (mode >= 1) and (mode <= 9), "Mode must be in the range [1, 9] (decimal)" - assert (pid >= 0) and (pid <= 196), "PID must be in the range [0, 196] (decimal)" - assert cmd.bytes >= 0, "Number of return bytes must be >= 0" - assert hasattr(cmd.decode, '__call__'), "Decode is not callable" + # make sure all the fields are set + assert cmd.name != "", "Command names must not be null" + assert cmd.name.isupper(), "Command names must be upper case" + assert ' ' not in cmd.name, "No spaces allowed in command names" + assert cmd.desc != "", "Command description must not be null" + assert (mode >= 1) and (mode <= 9), "Mode must be in the range [1, 9] (decimal)" + assert (pid >= 0) and (pid <= 196), "PID must be in the range [0, 196] (decimal)" + assert cmd.bytes >= 0, "Number of return bytes must be >= 0" + assert hasattr(cmd.decode, '__call__'), "Decode is not callable" def test_unique_names(): - # make sure no two commands have the same name - names = {} + # make sure no two commands have the same name + names = {} - for cmds in obd.commands.modes: - for cmd in cmds: - assert not names.__contains__(cmd.name), "Two commands share the same name: %s" % cmd.name - names[cmd.name] = True + for cmds in obd.commands.modes: + for cmd in cmds: + assert not names.__contains__(cmd.name), "Two commands share the same name: %s" % cmd.name + names[cmd.name] = True def test_getitem(): - # ensure that __getitem__ works correctly - for cmds in obd.commands.modes: - for cmd in cmds: + # ensure that __getitem__ works correctly + for cmds in obd.commands.modes: + for cmd in cmds: - # by [mode][pid] - mode = cmd.mode_int - pid = cmd.pid_int - assert cmd == obd.commands[mode][pid], "mode %d, PID %d could not be accessed through __getitem__" % (mode, pid) + # by [mode][pid] + mode = cmd.mode_int + pid = cmd.pid_int + assert cmd == obd.commands[mode][pid], "mode %d, PID %d could not be accessed through __getitem__" % (mode, pid) - # by [name] - assert cmd == obd.commands[cmd.name], "command name %s could not be accessed through __getitem__" % (cmd.name) + # by [name] + assert cmd == obd.commands[cmd.name], "command name %s could not be accessed through __getitem__" % (cmd.name) def test_contains(): - for cmds in obd.commands.modes: - for cmd in cmds: + for cmds in obd.commands.modes: + for cmd in cmds: - # by (command) - assert obd.commands.has_command(cmd) + # by (command) + assert obd.commands.has_command(cmd) - # by (mode, pid) - mode = cmd.mode_int - pid = cmd.pid_int - assert obd.commands.has_pid(mode, pid) + # by (mode, pid) + mode = cmd.mode_int + pid = cmd.pid_int + assert obd.commands.has_pid(mode, pid) - # by (name) - assert obd.commands.has_name(cmd.name) + # by (name) + assert obd.commands.has_name(cmd.name) - # by `in` - assert cmd.name in obd.commands + # by `in` + assert cmd.name in obd.commands - # test things NOT in the tables, or invalid parameters - assert 'modes' not in obd.commands - assert not obd.commands.has_pid(-1, 0) - assert not obd.commands.has_pid(1, -1) - assert not obd.commands.has_command("I'm a string, not an OBDCommand") + # test things NOT in the tables, or invalid parameters + assert 'modes' not in obd.commands + assert not obd.commands.has_pid(-1, 0) + assert not obd.commands.has_pid(1, -1) + assert not obd.commands.has_command("I'm a string, not an OBDCommand") def test_pid_getters(): - # ensure that all pid getters are found - pid_getters = obd.commands.pid_getters() + # ensure that all pid getters are found + pid_getters = obd.commands.pid_getters() - for cmds in obd.commands.modes: - for cmd in cmds: - if cmd.decode == pid: - assert cmd in pid_getters + for cmds in obd.commands.modes: + for cmd in cmds: + if cmd.decode == pid: + assert cmd in pid_getters diff --git a/tests/test_decoders.py b/tests/test_decoders.py index ae2083d7..f24cb8a8 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -4,161 +4,161 @@ def float_equals(d1, d2): - values_match = (abs(d1[0] - d2[0]) < 0.02) - units_match = (d1[1] == d2[1]) - return values_match and units_match + values_match = (abs(d1[0] - d2[0]) < 0.02) + units_match = (d1[1] == d2[1]) + return values_match and units_match def test_noop(): - assert d.noop("No Operation") == ("No Operation", Unit.NONE) + assert d.noop("No Operation") == ("No Operation", Unit.NONE) def test_pid(): - assert d.pid("00000000") == ("00000000000000000000000000000000", Unit.NONE) - assert d.pid("F00AA00F") == ("11110000000010101010000000001111", Unit.NONE) - assert d.pid("11") == ("00010001", Unit.NONE) + assert d.pid("00000000") == ("00000000000000000000000000000000", Unit.NONE) + assert d.pid("F00AA00F") == ("11110000000010101010000000001111", Unit.NONE) + assert d.pid("11") == ("00010001", Unit.NONE) def test_count(): - assert d.count("0") == (0, Unit.COUNT) - assert d.count("F") == (15, Unit.COUNT) - assert d.count("3E8") == (1000, Unit.COUNT) + assert d.count("0") == (0, Unit.COUNT) + assert d.count("F") == (15, Unit.COUNT) + assert d.count("3E8") == (1000, Unit.COUNT) def test_percent(): - assert d.percent("00") == (0.0, Unit.PERCENT) - assert d.percent("FF") == (100.0, Unit.PERCENT) + assert d.percent("00") == (0.0, Unit.PERCENT) + assert d.percent("FF") == (100.0, Unit.PERCENT) def test_percent_centered(): - assert d.percent_centered("00") == (-100.0, Unit.PERCENT) - assert d.percent_centered("80") == (0.0, Unit.PERCENT) - assert float_equals(d.percent_centered("FF"), (99.2, Unit.PERCENT)) + assert d.percent_centered("00") == (-100.0, Unit.PERCENT) + assert d.percent_centered("80") == (0.0, Unit.PERCENT) + assert float_equals(d.percent_centered("FF"), (99.2, Unit.PERCENT)) def test_temp(): - assert d.temp("00") == (-40, Unit.C) - assert d.temp("FF") == (215, Unit.C) - assert d.temp("3E8") == (960, Unit.C) + assert d.temp("00") == (-40, Unit.C) + assert d.temp("FF") == (215, Unit.C) + assert d.temp("3E8") == (960, Unit.C) def test_catalyst_temp(): - assert d.catalyst_temp("0000") == (-40.0, Unit.C) - assert d.catalyst_temp("FFFF") == (6513.5, Unit.C) + assert d.catalyst_temp("0000") == (-40.0, Unit.C) + assert d.catalyst_temp("FFFF") == (6513.5, Unit.C) def test_current_centered(): - assert d.current_centered("00000000") == (-128.0, Unit.MA) - assert d.current_centered("00008000") == (0.0, Unit.MA) - assert float_equals(d.current_centered("0000FFFF"), (128.0, Unit.MA)) - assert d.current_centered("ABCD8000") == (0.0, Unit.MA) # first 2 bytes are unused (should be disregarded) + assert d.current_centered("00000000") == (-128.0, Unit.MA) + assert d.current_centered("00008000") == (0.0, Unit.MA) + assert float_equals(d.current_centered("0000FFFF"), (128.0, Unit.MA)) + assert d.current_centered("ABCD8000") == (0.0, Unit.MA) # first 2 bytes are unused (should be disregarded) def test_sensor_voltage(): - assert d.sensor_voltage("0000") == (0.0, Unit.VOLT) - assert d.sensor_voltage("FFFF") == (1.275, Unit.VOLT) + assert d.sensor_voltage("0000") == (0.0, Unit.VOLT) + assert d.sensor_voltage("FFFF") == (1.275, Unit.VOLT) def test_sensor_voltage_big(): - assert d.sensor_voltage_big("00000000") == (0.0, Unit.VOLT) - assert float_equals(d.sensor_voltage_big("00008000"), (4.0, Unit.VOLT)) - assert d.sensor_voltage_big("0000FFFF") == (8.0, Unit.VOLT) - assert d.sensor_voltage_big("ABCD0000") == (0.0, Unit.VOLT) # first 2 bytes are unused (should be disregarded) + assert d.sensor_voltage_big("00000000") == (0.0, Unit.VOLT) + assert float_equals(d.sensor_voltage_big("00008000"), (4.0, Unit.VOLT)) + assert d.sensor_voltage_big("0000FFFF") == (8.0, Unit.VOLT) + assert d.sensor_voltage_big("ABCD0000") == (0.0, Unit.VOLT) # first 2 bytes are unused (should be disregarded) def test_fuel_pressure(): - assert d.fuel_pressure("00") == (0, Unit.KPA) - assert d.fuel_pressure("80") == (384, Unit.KPA) - assert d.fuel_pressure("FF") == (765, Unit.KPA) + assert d.fuel_pressure("00") == (0, Unit.KPA) + assert d.fuel_pressure("80") == (384, Unit.KPA) + assert d.fuel_pressure("FF") == (765, Unit.KPA) def test_pressure(): - assert d.pressure("00") == (0, Unit.KPA) - assert d.pressure("00") == (0, Unit.KPA) + assert d.pressure("00") == (0, Unit.KPA) + assert d.pressure("00") == (0, Unit.KPA) def test_fuel_pres_vac(): - assert d.fuel_pres_vac("0000") == (0.0, Unit.KPA) - assert d.fuel_pres_vac("FFFF") == (5177.265, Unit.KPA) + assert d.fuel_pres_vac("0000") == (0.0, Unit.KPA) + assert d.fuel_pres_vac("FFFF") == (5177.265, Unit.KPA) def test_fuel_pres_direct(): - assert d.fuel_pres_direct("0000") == (0, Unit.KPA) - assert d.fuel_pres_direct("FFFF") == (655350, Unit.KPA) + assert d.fuel_pres_direct("0000") == (0, Unit.KPA) + assert d.fuel_pres_direct("FFFF") == (655350, Unit.KPA) def test_evap_pressure(): - pass - #assert d.evap_pressure("0000") == (0.0, Unit.PA) + pass + #assert d.evap_pressure("0000") == (0.0, Unit.PA) def test_abs_evap_pressure(): - assert d.abs_evap_pressure("0000") == (0, Unit.KPA) - assert d.abs_evap_pressure("FFFF") == (327.675, Unit.KPA) + assert d.abs_evap_pressure("0000") == (0, Unit.KPA) + assert d.abs_evap_pressure("FFFF") == (327.675, Unit.KPA) def test_evap_pressure_alt(): - assert d.evap_pressure_alt("0000") == (-32767, Unit.PA) - assert d.evap_pressure_alt("7FFF") == (0, Unit.PA) - assert d.evap_pressure_alt("FFFF") == (32768, Unit.PA) + assert d.evap_pressure_alt("0000") == (-32767, Unit.PA) + assert d.evap_pressure_alt("7FFF") == (0, Unit.PA) + assert d.evap_pressure_alt("FFFF") == (32768, Unit.PA) def test_rpm(): - assert d.rpm("0000") == (0.0, Unit.RPM) - assert d.rpm("FFFF") == (16383.75, Unit.RPM) + assert d.rpm("0000") == (0.0, Unit.RPM) + assert d.rpm("FFFF") == (16383.75, Unit.RPM) def test_speed(): - assert d.speed("00") == (0, Unit.KPH) - assert d.speed("FF") == (255, Unit.KPH) + assert d.speed("00") == (0, Unit.KPH) + assert d.speed("FF") == (255, Unit.KPH) def test_timing_advance(): - assert d.timing_advance("00") == (-64.0, Unit.DEGREES) - assert d.timing_advance("FF") == (63.5, Unit.DEGREES) + assert d.timing_advance("00") == (-64.0, Unit.DEGREES) + assert d.timing_advance("FF") == (63.5, Unit.DEGREES) def test_inject_timing(): - assert d.inject_timing("0000") == (-210, Unit.DEGREES) - assert float_equals(d.inject_timing("FFFF"), (302, Unit.DEGREES)) + assert d.inject_timing("0000") == (-210, Unit.DEGREES) + assert float_equals(d.inject_timing("FFFF"), (302, Unit.DEGREES)) def test_maf(): - assert d.maf("0000") == (0.0, Unit.GPS) - assert d.maf("FFFF") == (655.35, Unit.GPS) + assert d.maf("0000") == (0.0, Unit.GPS) + assert d.maf("FFFF") == (655.35, Unit.GPS) def test_max_maf(): - assert d.max_maf("00000000") == (0, Unit.GPS) - assert d.max_maf("FF000000") == (2550, Unit.GPS) - assert d.max_maf("00ABCDEF") == (0, Unit.GPS) # last 3 bytes are unused (should be disregarded) + assert d.max_maf("00000000") == (0, Unit.GPS) + assert d.max_maf("FF000000") == (2550, Unit.GPS) + assert d.max_maf("00ABCDEF") == (0, Unit.GPS) # last 3 bytes are unused (should be disregarded) def test_seconds(): - assert d.seconds("0000") == (0, Unit.SEC) - assert d.seconds("FFFF") == (65535, Unit.SEC) + assert d.seconds("0000") == (0, Unit.SEC) + assert d.seconds("FFFF") == (65535, Unit.SEC) def test_minutes(): - assert d.minutes("0000") == (0, Unit.MIN) - assert d.minutes("FFFF") == (65535, Unit.MIN) + assert d.minutes("0000") == (0, Unit.MIN) + assert d.minutes("FFFF") == (65535, Unit.MIN) def test_distance(): - assert d.distance("0000") == (0, Unit.KM) - assert d.distance("FFFF") == (65535, Unit.KM) + assert d.distance("0000") == (0, Unit.KM) + assert d.distance("FFFF") == (65535, Unit.KM) def test_fuel_rate(): - assert d.fuel_rate("0000") == (0.0, Unit.LPH) - assert d.fuel_rate("FFFF") == (3276.75, Unit.LPH) + assert d.fuel_rate("0000") == (0.0, Unit.LPH) + assert d.fuel_rate("FFFF") == (3276.75, Unit.LPH) def test_fuel_status(): - assert d.fuel_status("0100") == ("Open loop due to insufficient engine temperature", Unit.NONE) - assert d.fuel_status("0800") == ("Open loop due to system failure", Unit.NONE) - assert d.fuel_status("0300") == (None, Unit.NONE) + assert d.fuel_status("0100") == ("Open loop due to insufficient engine temperature", Unit.NONE) + assert d.fuel_status("0800") == ("Open loop due to system failure", Unit.NONE) + assert d.fuel_status("0300") == (None, Unit.NONE) def test_air_status(): - assert d.air_status("01") == ("Upstream", Unit.NONE) - assert d.air_status("08") == ("Pump commanded on for diagnostics", Unit.NONE) - assert d.air_status("03") == (None, Unit.NONE) + assert d.air_status("01") == ("Upstream", Unit.NONE) + assert d.air_status("08") == ("Pump commanded on for diagnostics", Unit.NONE) + assert d.air_status("03") == (None, Unit.NONE) def test_dtc(): - assert d.dtc("0104") == ([ - ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), - ], Unit.NONE) - - # multiple codes - assert d.dtc("010480034123") == ([ - ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), - ("B0003", "Unknown error code"), - ("C0123", "Unknown error code"), - ], Unit.NONE) - - # invalid code lengths are dropped - assert d.dtc("01048003412") == ([ - ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), - ("B0003", "Unknown error code"), - ], Unit.NONE) - - # 0000 codes are dropped - assert d.dtc("000001040000") == ([ - ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), - ], Unit.NONE) + assert d.dtc("0104") == ([ + ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), + ], Unit.NONE) + + # multiple codes + assert d.dtc("010480034123") == ([ + ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), + ("B0003", "Unknown error code"), + ("C0123", "Unknown error code"), + ], Unit.NONE) + + # invalid code lengths are dropped + assert d.dtc("01048003412") == ([ + ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), + ("B0003", "Unknown error code"), + ], Unit.NONE) + + # 0000 codes are dropped + assert d.dtc("000001040000") == ([ + ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), + ], Unit.NONE) diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index 3601049b..0bf6ee04 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -8,142 +8,142 @@ CAN_11_PROTOCOLS = [ - ISO_15765_4_11bit_500k, - ISO_15765_4_11bit_250k, + ISO_15765_4_11bit_500k, + ISO_15765_4_11bit_250k, ] CAN_29_PROTOCOLS = [ - ISO_15765_4_29bit_500k, - ISO_15765_4_29bit_250k, - SAE_J1939 + ISO_15765_4_29bit_500k, + ISO_15765_4_29bit_250k, + SAE_J1939 ] def check_message(m, num_frames, tx_id, data): - """ generic test for correct message values """ - assert len(m.frames) == num_frames - assert m.tx_id == tx_id - assert m.data == data + """ generic test for correct message values """ + assert len(m.frames) == num_frames + assert m.tx_id == tx_id + assert m.data == data def test_single_frame(): - for protocol in CAN_11_PROTOCOLS: - p = protocol([]) + for protocol in CAN_11_PROTOCOLS: + p = protocol([]) - r = p(["7E8 06 41 00 00 01 02 03"]) - assert len(r) == 1 - check_message(r[0], 1, 0x0, list(range(4))) + r = p(["7E8 06 41 00 00 01 02 03"]) + assert len(r) == 1 + check_message(r[0], 1, 0x0, list(range(4))) def test_hex_straining(): - for protocol in CAN_11_PROTOCOLS: - p = protocol([]) + for protocol in CAN_11_PROTOCOLS: + p = protocol([]) - r = p(["NO DATA"]) - assert len(r) == 0 + r = p(["NO DATA"]) + assert len(r) == 0 - r = p(["TOTALLY NOT HEX"]) - assert len(r) == 0 + r = p(["TOTALLY NOT HEX"]) + assert len(r) == 0 - r = p(["NO DATA", "7E8 06 41 00 00 01 02 03"]) - assert len(r) == 1 - check_message(r[0], 1, 0x0, list(range(4))) + r = p(["NO DATA", "7E8 06 41 00 00 01 02 03"]) + assert len(r) == 1 + check_message(r[0], 1, 0x0, list(range(4))) - r = p(["NO DATA", "NO DATA"]) - assert len(r) == 0 + r = p(["NO DATA", "NO DATA"]) + assert len(r) == 0 def test_multi_ecu(): - for protocol in CAN_11_PROTOCOLS: - p = protocol([]) + for protocol in CAN_11_PROTOCOLS: + p = protocol([]) - test_case = [ - "7E8 06 41 00 00 01 02 03", - "7EB 06 41 00 00 01 02 03", - "7EA 06 41 00 00 01 02 03", - ] + test_case = [ + "7E8 06 41 00 00 01 02 03", + "7EB 06 41 00 00 01 02 03", + "7EA 06 41 00 00 01 02 03", + ] - correct_data = list(range(4)) + correct_data = list(range(4)) - # seperate ECUs, single frames each - r = p(test_case) - assert len(r) == 3 + # seperate ECUs, single frames each + r = p(test_case) + assert len(r) == 3 - # messages are returned in ECU order - check_message(r[0], 1, 0x0, correct_data) - check_message(r[1], 1, 0x2, correct_data) - check_message(r[2], 1, 0x3, correct_data) + # messages are returned in ECU order + check_message(r[0], 1, 0x0, correct_data) + check_message(r[1], 1, 0x2, correct_data) + check_message(r[2], 1, 0x3, correct_data) def test_multi_line(): - for protocol in CAN_11_PROTOCOLS: - p = protocol([]) + for protocol in CAN_11_PROTOCOLS: + p = protocol([]) - test_case = [ - "7E8 10 20 49 04 00 01 02 03", - "7E8 21 04 05 06 07 08 09 0A", - "7E8 22 0B 0C 0D 0E 0F 10 11", - "7E8 23 12 13 14 15 16 17 18" - ] + test_case = [ + "7E8 10 20 49 04 00 01 02 03", + "7E8 21 04 05 06 07 08 09 0A", + "7E8 22 0B 0C 0D 0E 0F 10 11", + "7E8 23 12 13 14 15 16 17 18" + ] - correct_data = list(range(25)) + correct_data = list(range(25)) - # in-order - r = p(test_case) - assert len(r) == 1 - check_message(r[0], len(test_case), 0x0, correct_data) + # in-order + r = p(test_case) + assert len(r) == 1 + check_message(r[0], len(test_case), 0x0, correct_data) - # test a few out-of-order cases - for n in range(4): - random.shuffle(test_case) # mix up the frame strings - r = p(test_case) - assert len(r) == 1 - check_message(r[0], len(test_case), 0x0, correct_data) + # test a few out-of-order cases + for n in range(4): + random.shuffle(test_case) # mix up the frame strings + r = p(test_case) + assert len(r) == 1 + check_message(r[0], len(test_case), 0x0, correct_data) - # missing frames in a multi-frame message should drop the message - # (tests the contiguity check, and data length byte) + # missing frames in a multi-frame message should drop the message + # (tests the contiguity check, and data length byte) - test_case = [ - "7E8 10 20 49 04 00 01 02 03", - "7E8 21 04 05 06 07 08 09 0A", - "7E8 22 0B 0C 0D 0E 0F 10 11", - "7E8 23 12 13 14 15 16 17 18" - ] + test_case = [ + "7E8 10 20 49 04 00 01 02 03", + "7E8 21 04 05 06 07 08 09 0A", + "7E8 22 0B 0C 0D 0E 0F 10 11", + "7E8 23 12 13 14 15 16 17 18" + ] - for n in range(len(test_case) - 1): - sub_test = list(test_case) - del sub_test[n] + for n in range(len(test_case) - 1): + sub_test = list(test_case) + del sub_test[n] - r = p(sub_test) - assert len(r) == 0 + r = p(sub_test) + assert len(r) == 0 - # MODE 03 COMMANDS (GET_DTC) RETURN NO PID BYTE + # MODE 03 COMMANDS (GET_DTC) RETURN NO PID BYTE - test_case = [ - "7E8 10 20 43 04 00 01 02 03", - "7E8 21 04 05 06 07 08 09 0A", - ] + test_case = [ + "7E8 10 20 43 04 00 01 02 03", + "7E8 21 04 05 06 07 08 09 0A", + ] - correct_data = list(range(8)) + correct_data = list(range(8)) - r = p(test_case) - assert len(r) == 1 - check_message(r[0], len(test_case), 0, correct_data) + r = p(test_case) + assert len(r) == 1 + check_message(r[0], len(test_case), 0, correct_data) def test_can_29(): - pass + pass diff --git a/tests/test_protocol_legacy.py b/tests/test_protocol_legacy.py index 49a78b95..61aae50e 100644 --- a/tests/test_protocol_legacy.py +++ b/tests/test_protocol_legacy.py @@ -8,179 +8,179 @@ LEGACY_PROTOCOLS = [ - SAE_J1850_PWM, - SAE_J1850_VPW, - ISO_9141_2, - ISO_14230_4_5baud, - ISO_14230_4_fast + SAE_J1850_PWM, + SAE_J1850_VPW, + ISO_9141_2, + ISO_14230_4_5baud, + ISO_14230_4_fast ] def check_message(m, n_frames, tx_id, data): - """ generic test for correct message values """ - assert len(m.frames) == n_frames - assert m.tx_id == tx_id - assert m.data == data + """ generic test for correct message values """ + assert len(m.frames) == n_frames + assert m.tx_id == tx_id + assert m.data == data def test_single_frame(): - for protocol in LEGACY_PROTOCOLS: - p = protocol([]) + for protocol in LEGACY_PROTOCOLS: + p = protocol([]) - # minimum valid length - r = p(["48 6B 10 41 00 FF"]) - assert len(r) == 1 - check_message(r[0], 1, 0x10, []) + # minimum valid length + r = p(["48 6B 10 41 00 FF"]) + assert len(r) == 1 + check_message(r[0], 1, 0x10, []) - # maximum valid length - r = p(["48 6B 10 41 00 00 01 02 03 04 FF"]) - assert len(r) == 1 - check_message(r[0], 1, 0x10, list(range(5))) + # maximum valid length + r = p(["48 6B 10 41 00 00 01 02 03 04 FF"]) + assert len(r) == 1 + check_message(r[0], 1, 0x10, list(range(5))) - # to short - r = p(["48 6B 10 41 FF"]) - assert len(r) == 0 + # to short + r = p(["48 6B 10 41 FF"]) + assert len(r) == 0 - # to long - r = p(["48 6B 10 41 00 00 01 02 03 04 05 FF"]) - assert len(r) == 0 + # to long + r = p(["48 6B 10 41 00 00 01 02 03 04 05 FF"]) + assert len(r) == 0 def test_hex_straining(): - """ - If non-hex values are sent, they should be marked as ECU.UNKNOWN - """ + """ + If non-hex values are sent, they should be marked as ECU.UNKNOWN + """ - for protocol in LEGACY_PROTOCOLS: - p = protocol([]) + for protocol in LEGACY_PROTOCOLS: + p = protocol([]) - # single non-hex message - r = p(["12.8 Volts"]) - assert len(r) == 1 - assert r[0].ecu == ECU.UNKNOWN - assert len(r[0].frames) == 1 + # single non-hex message + r = p(["12.8 Volts"]) + assert len(r) == 1 + assert r[0].ecu == ECU.UNKNOWN + assert len(r[0].frames) == 1 - # multiple non-hex message - r = p(["12.8 Volts", "NO DATA"]) - assert len(r) == 2 + # multiple non-hex message + r = p(["12.8 Volts", "NO DATA"]) + assert len(r) == 2 - for m in r: - assert m.ecu == ECU.UNKNOWN - assert len(m.frames) == 1 - - # mixed hex and non-hex - r = p(["NO DATA", "48 6B 10 41 00 00 01 02 03 FF"]) - assert len(r) == 2 + for m in r: + assert m.ecu == ECU.UNKNOWN + assert len(m.frames) == 1 + + # mixed hex and non-hex + r = p(["NO DATA", "48 6B 10 41 00 00 01 02 03 FF"]) + assert len(r) == 2 - # first message should be the valid, parsable hex message - # NOTE: the parser happens to process the valid one's first - check_message(r[0], 1, 0x10, list(range(4))) + # first message should be the valid, parsable hex message + # NOTE: the parser happens to process the valid one's first + check_message(r[0], 1, 0x10, list(range(4))) - # second message: invalid, non-parsable non-hex - assert r[1].ecu == ECU.UNKNOWN - assert len(r[1].frames) == 1 - assert len(r[1].data) == 0 # no data + # second message: invalid, non-parsable non-hex + assert r[1].ecu == ECU.UNKNOWN + assert len(r[1].frames) == 1 + assert len(r[1].data) == 0 # no data def test_multi_ecu(): - for protocol in LEGACY_PROTOCOLS: - p = protocol([]) + for protocol in LEGACY_PROTOCOLS: + p = protocol([]) - test_case = [ - "48 6B 13 41 00 00 01 02 03 FF", - "48 6B 10 41 00 00 01 02 03 FF", - "48 6B 11 41 00 00 01 02 03 FF", - ] + test_case = [ + "48 6B 13 41 00 00 01 02 03 FF", + "48 6B 10 41 00 00 01 02 03 FF", + "48 6B 11 41 00 00 01 02 03 FF", + ] - correct_data = list(range(4)) + correct_data = list(range(4)) - # seperate ECUs, single frames each - r = p(test_case) - assert len(r) == len(test_case) + # seperate ECUs, single frames each + r = p(test_case) + assert len(r) == len(test_case) - # messages are returned in ECU order - check_message(r[0], 1, 0x10, correct_data) - check_message(r[1], 1, 0x11, correct_data) - check_message(r[2], 1, 0x13, correct_data) + # messages are returned in ECU order + check_message(r[0], 1, 0x10, correct_data) + check_message(r[1], 1, 0x11, correct_data) + check_message(r[2], 1, 0x13, correct_data) def test_multi_line(): - """ - Tests that valid multiline messages are recombined into single - messages. - """ + """ + Tests that valid multiline messages are recombined into single + messages. + """ - for protocol in LEGACY_PROTOCOLS: - p = protocol([]) + for protocol in LEGACY_PROTOCOLS: + p = protocol([]) - test_case = [ - "48 6B 10 49 02 01 00 01 02 03 FF", - "48 6B 10 49 02 02 04 05 06 07 FF", - "48 6B 10 49 02 03 08 09 0A 0B FF", - ] + test_case = [ + "48 6B 10 49 02 01 00 01 02 03 FF", + "48 6B 10 49 02 02 04 05 06 07 FF", + "48 6B 10 49 02 03 08 09 0A 0B FF", + ] - correct_data = list(range(12)) + correct_data = list(range(12)) - # in-order - r = p(test_case) - assert len(r) == 1 - check_message(r[0], len(test_case), 0x10, correct_data) + # in-order + r = p(test_case) + assert len(r) == 1 + check_message(r[0], len(test_case), 0x10, correct_data) - # test a few out-of-order cases - for n in range(4): - random.shuffle(test_case) # mix up the frame strings - r = p(test_case) - assert len(r) == 1 - check_message(r[0], len(test_case), 0x10, correct_data) + # test a few out-of-order cases + for n in range(4): + random.shuffle(test_case) # mix up the frame strings + r = p(test_case) + assert len(r) == 1 + check_message(r[0], len(test_case), 0x10, correct_data) def test_multi_line_missing_frames(): - """ - Missing frames in a multi-frame message should drop the message. - Tests the contiguity check, and data length byte - """ + """ + Missing frames in a multi-frame message should drop the message. + Tests the contiguity check, and data length byte + """ - for protocol in LEGACY_PROTOCOLS: - p = protocol([]) + for protocol in LEGACY_PROTOCOLS: + p = protocol([]) - test_case = [ - "48 6B 10 49 02 01 00 01 02 03 FF", - "48 6B 10 49 02 02 04 05 06 07 FF", - "48 6B 10 49 02 03 08 09 0A 0B FF", - ] + test_case = [ + "48 6B 10 49 02 01 00 01 02 03 FF", + "48 6B 10 49 02 02 04 05 06 07 FF", + "48 6B 10 49 02 03 08 09 0A 0B FF", + ] - for n in range(len(test_case) - 1): - sub_test = list(test_case) - del sub_test[n] + for n in range(len(test_case) - 1): + sub_test = list(test_case) + del sub_test[n] - r = p(sub_test) - assert len(r) == 0 + r = p(sub_test) + assert len(r) == 0 def test_multi_line_mode_03(): - """ - Tests the special handling of mode 3 commands. - Namely, Mode 03 commands (GET_DTC) return no PID byte. - When frames are combined, the parser should account for this. - """ + """ + Tests the special handling of mode 3 commands. + Namely, Mode 03 commands (GET_DTC) return no PID byte. + When frames are combined, the parser should account for this. + """ - for protocol in LEGACY_PROTOCOLS: - p = protocol([]) + for protocol in LEGACY_PROTOCOLS: + p = protocol([]) - test_case = [ - "48 6B 10 43 00 01 02 03 04 05 FF", - "48 6B 10 43 06 07 08 09 0A 0B FF", - ] + test_case = [ + "48 6B 10 43 00 01 02 03 04 05 FF", + "48 6B 10 43 06 07 08 09 0A 0B FF", + ] - correct_data = list(range(12)) # data is stitched in order recieved + correct_data = list(range(12)) # data is stitched in order recieved - r = p(test_case) - assert len(r) == 1 - check_message(r[0], len(test_case), 0x10, correct_data) + r = p(test_case) + assert len(r) == 1 + check_message(r[0], len(test_case), 0x10, correct_data) From 29d80dc300f8acb977ec5545285fe06aa8c0dfd6 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jan 2016 00:46:50 -0500 Subject: [PATCH 288/569] fixed CAN 11 bit tests --- tests/test_protocol_can.py | 62 +++++++++++++++++++++++++++-------- tests/test_protocol_legacy.py | 2 +- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index 0bf6ee04..2ab97988 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -27,8 +27,6 @@ def check_message(m, num_frames, tx_id, data): - - def test_single_frame(): for protocol in CAN_11_PROTOCOLS: p = protocol([]) @@ -41,22 +39,40 @@ def test_single_frame(): def test_hex_straining(): + """ + If non-hex values are sent, they should be marked as ECU.UNKNOWN + """ + for protocol in CAN_11_PROTOCOLS: p = protocol([]) + # single non-hex message + r = p(["12.8 Volts"]) + assert len(r) == 1 + assert r[0].ecu == ECU.UNKNOWN + assert len(r[0].frames) == 1 + - r = p(["NO DATA"]) - assert len(r) == 0 + # multiple non-hex message + r = p(["12.8 Volts", "NO DATA"]) + assert len(r) == 2 - r = p(["TOTALLY NOT HEX"]) - assert len(r) == 0 + for m in r: + assert m.ecu == ECU.UNKNOWN + assert len(m.frames) == 1 + # mixed hex and non-hex r = p(["NO DATA", "7E8 06 41 00 00 01 02 03"]) - assert len(r) == 1 + assert len(r) == 2 + + # first message should be the valid, parsable hex message + # NOTE: the parser happens to process the valid one's first check_message(r[0], 1, 0x0, list(range(4))) - r = p(["NO DATA", "NO DATA"]) - assert len(r) == 0 + # second message: invalid, non-parsable non-hex + assert r[1].ecu == ECU.UNKNOWN + assert len(r[1].frames) == 1 + assert len(r[1].data) == 0 # no data @@ -84,8 +100,12 @@ def test_multi_ecu(): - def test_multi_line(): + """ + Tests that valid multiline messages are recombined into single + messages. + """ + for protocol in CAN_11_PROTOCOLS: p = protocol([]) @@ -112,8 +132,15 @@ def test_multi_line(): check_message(r[0], len(test_case), 0x0, correct_data) - # missing frames in a multi-frame message should drop the message - # (tests the contiguity check, and data length byte) + +def test_multi_line_missing_frames(): + """ + Missing frames in a multi-frame message should drop the message. + Tests the contiguity check, and data length byte + """ + + for protocol in CAN_11_PROTOCOLS: + p = protocol([]) test_case = [ "7E8 10 20 49 04 00 01 02 03", @@ -130,7 +157,16 @@ def test_multi_line(): assert len(r) == 0 - # MODE 03 COMMANDS (GET_DTC) RETURN NO PID BYTE + +def test_multi_line_mode_03(): + """ + Tests the special handling of mode 3 commands. + Namely, Mode 03 commands (GET_DTC) return no PID byte. + When frames are combined, the parser should account for this. + """ + + for protocol in CAN_11_PROTOCOLS: + p = protocol([]) test_case = [ "7E8 10 20 43 04 00 01 02 03", diff --git a/tests/test_protocol_legacy.py b/tests/test_protocol_legacy.py index 61aae50e..142e4b67 100644 --- a/tests/test_protocol_legacy.py +++ b/tests/test_protocol_legacy.py @@ -68,7 +68,7 @@ def test_hex_straining(): for m in r: assert m.ecu == ECU.UNKNOWN assert len(m.frames) == 1 - + # mixed hex and non-hex r = p(["NO DATA", "48 6B 10 41 00 00 01 02 03 FF"]) assert len(r) == 2 From d9ab077313dc96adb1c82e592df434d10ecf77ed Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jan 2016 01:04:45 -0500 Subject: [PATCH 289/569] put in notes about testing for invalid lengths --- obd/protocols/protocol_can.py | 13 +++++++++++++ tests/test_protocol_can.py | 7 ++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 9b07cbef..8abe7bc4 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -65,6 +65,19 @@ def parse_frame(self, frame): raw_bytes = ascii_to_bytes(raw) + # check for valid size + + # TODO: lookup this limit + # if len(raw_bytes) < 9: + # debug("Dropped frame for being too short") + # return False + + # TODO: lookup this limit + # if len(raw_bytes) > 16: + # debug("Dropped frame for being too long") + # return False + + # read header information if self.id_bits == 11: # Ex. diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index 2ab97988..95eef542 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -37,6 +37,12 @@ def test_single_frame(): check_message(r[0], 1, 0x0, list(range(4))) + r = p(["7E8 08 41 00 00 01 02 03 04 05"]) + assert len(r) == 1 + check_message(r[0], 1, 0x0, list(range(6))) + + # TODO: check for invalid length filterring + def test_hex_straining(): """ @@ -109,7 +115,6 @@ def test_multi_line(): for protocol in CAN_11_PROTOCOLS: p = protocol([]) - test_case = [ "7E8 10 20 49 04 00 01 02 03", "7E8 21 04 05 06 07 08 09 0A", From cd61b1fe87ee8bb392187ac7f3f306089a8d93b7 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jan 2016 01:24:26 -0500 Subject: [PATCH 290/569] fixed tests for OBDCommand --- tests/test_OBDCommand.py | 43 ++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index 480a55fd..09751b32 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -1,11 +1,21 @@ -from obd.commands import OBDCommand +from obd.OBDCommand import OBDCommand +from obd.OBDResponse import Unit from obd.decoders import noop from obd.protocols import * + +def decode_raw(messages): + """ a passiver decoder """ + return (messages[0].data, Unit.NONE) + + + def test_constructor(): - # name description mode cmd bytes decoder + + # default constructor + # name description cmd bytes decoder ECU cmd = OBDCommand("Test", "example OBD command", "0123", 2, noop, ECU.ENGINE) assert cmd.name == "Test" assert cmd.desc == "example OBD command" @@ -13,15 +23,20 @@ def test_constructor(): assert cmd.bytes == 2 assert cmd.decode == noop assert cmd.ecu == ECU.ENGINE + assert cmd.fast == False assert cmd.supported == False - assert cmd.mode_int == 1 - assert cmd.pid_int == 35 + assert cmd.mode_int == 1 + assert cmd.pid_int == 35 - cmd = OBDCommand("Test", "example OBD command", "0123", 2, noop, ECU.ENGINE, True) + # a case where "fast", and "supported" were set explicitly + # name description cmd bytes decoder ECU fast supported + cmd = OBDCommand("Test 2", "example OBD command", "0123", 2, noop, ECU.ENGINE, True, True) + assert cmd.fast == True assert cmd.supported == True + def test_clone(): # name description mode cmd bytes decoder cmd = OBDCommand("", "", "0123", 2, noop, ECU.ENGINE) @@ -33,27 +48,32 @@ def test_clone(): assert cmd.bytes == other.bytes assert cmd.decode == other.decode assert cmd.ecu == other.ecu + assert cmd.fast == cmd.fast assert cmd.supported == cmd.supported + def test_call(): p = SAE_J1850_PWM(["48 6B 10 41 00 FF FF FF FF AA"]) # train the ecu_map to identify the engine messages = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) # parse valid data into response object + print(messages[0].data) + # valid response size - cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) + cmd = OBDCommand("", "", "0123", 4, decode_raw, ECU.ENGINE) r = cmd(messages) - assert r.value == "BE1FB811" + assert r.value == [0xBE, 0x1F, 0xB8, 0x11] # response too short (pad) - cmd = OBDCommand("", "", "0123", 5, noop, ECU.ENGINE) + cmd = OBDCommand("", "", "0123", 5, decode_raw, ECU.ENGINE) r = cmd(messages) - assert r.value == "BE1FB81100" + assert r.value == [0xBE, 0x1F, 0xB8, 0x11, 0x00] # response too long (clip) - cmd = OBDCommand("", "", "0123", 3, noop, ECU.ENGINE) + cmd = OBDCommand("", "", "0123", 3, decode_raw, ECU.ENGINE) r = cmd(messages) - assert r.value == "BE1FB8" + assert r.value == [0xBE, 0x1F, 0xB8] + def test_get_mode_int(): @@ -64,6 +84,7 @@ def test_get_mode_int(): assert cmd.mode_int == 0 + def test_pid_int(): cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) assert cmd.pid_int == 0x23 From 110f06e9c58af45c64a2b650d4fd62096478752c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jan 2016 01:34:34 -0500 Subject: [PATCH 291/569] rewrote the first couple decoder tests --- tests/test_decoders.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/test_decoders.py b/tests/test_decoders.py index f24cb8a8..d19b77c7 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -1,8 +1,18 @@ from obd.OBDResponse import Unit +from obd.protocols.protocol import Message import obd.decoders as d +# returns a list with a single valid message, +# containing the requested data +def m(data, frames=[]): + # most decoders don't look at the underlying frame objects + message = Message(frames) + message.data = data + return [message] + + def float_equals(d1, d2): values_match = (abs(d1[0] - d2[0]) < 0.02) units_match = (d1[1] == d2[1]) @@ -13,12 +23,12 @@ def float_equals(d1, d2): def test_noop(): - assert d.noop("No Operation") == ("No Operation", Unit.NONE) + assert d.noop(m("any odd input")) == (None, Unit.NONE) def test_pid(): - assert d.pid("00000000") == ("00000000000000000000000000000000", Unit.NONE) - assert d.pid("F00AA00F") == ("11110000000010101010000000001111", Unit.NONE) - assert d.pid("11") == ("00010001", Unit.NONE) + assert d.pid(m([0x00, 0x00, 0x00, 0x00])) == ("00000000000000000000000000000000", Unit.NONE) + assert d.pid(m([0xF0, 0x0A, 0xA0, 0x0F])) == ("11110000000010101010000000001111", Unit.NONE) + assert d.pid(m([0x11])) == ("00010001", Unit.NONE) def test_count(): assert d.count("0") == (0, Unit.COUNT) From 39db1f5d5312f7fdb70c22b6b9cac38f66fdea60 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jan 2016 01:42:46 -0500 Subject: [PATCH 292/569] using hex strings for readability --- tests/test_decoders.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_decoders.py b/tests/test_decoders.py index d19b77c7..9788567e 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -1,4 +1,6 @@ +from binascii import unhexlify + from obd.OBDResponse import Unit from obd.protocols.protocol import Message import obd.decoders as d @@ -6,10 +8,10 @@ # returns a list with a single valid message, # containing the requested data -def m(data, frames=[]): +def m(hex_data, frames=[]): # most decoders don't look at the underlying frame objects message = Message(frames) - message.data = data + message.data = list(unhexlify(hex_data)) # TODO: use raw byte arrays return [message] @@ -23,12 +25,12 @@ def float_equals(d1, d2): def test_noop(): - assert d.noop(m("any odd input")) == (None, Unit.NONE) + assert d.noop(m("deadbeef")) == (None, Unit.NONE) def test_pid(): - assert d.pid(m([0x00, 0x00, 0x00, 0x00])) == ("00000000000000000000000000000000", Unit.NONE) - assert d.pid(m([0xF0, 0x0A, 0xA0, 0x0F])) == ("11110000000010101010000000001111", Unit.NONE) - assert d.pid(m([0x11])) == ("00010001", Unit.NONE) + assert d.pid(m("00000000")) == ("00000000000000000000000000000000", Unit.NONE) + assert d.pid(m("F00AA00F")) == ("11110000000010101010000000001111", Unit.NONE) + assert d.pid(m("11")) == ("00010001", Unit.NONE) def test_count(): assert d.count("0") == (0, Unit.COUNT) From 1359bbc915927b1a794e21d05a2b78dcd8c6a69c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jan 2016 01:55:53 -0500 Subject: [PATCH 293/569] fixed decoder tests, handle invalid byte counts in DTC decoder --- obd/decoders.py | 12 ++-- tests/test_decoders.py | 140 ++++++++++++++++++++--------------------- 2 files changed, 77 insertions(+), 75 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index c704c8c1..bda9cb76 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -38,7 +38,7 @@ ''' All decoders take the form: -def (_hex): +def (): ... return (, ) @@ -94,8 +94,9 @@ def catalyst_temp(messages): return (v, Unit.C) # -128 to 128 mA -def current_centered(_hex): - v = unhex(_hex[4:8]) +def current_centered(messages): + d = messages[0].data + v = bytes_to_int(d[2:4]) v = (v / 256.0) - 128 return (v, Unit.MA) @@ -391,10 +392,11 @@ def dtc(messages): print(bytes_to_hex(d)) # look at data in pairs of bytes - for n in range(0, len(d), 2): + # looping through ENDING indices to avoid odd (invalid) code lengths + for n in range(1, len(d), 2): # parse the code - dtc = single_dtc( (d[n], d[n+1]) ) + dtc = single_dtc( (d[n-1], d[n]) ) if dtc is not None: # pull a description if we have one diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 9788567e..edb083d4 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -33,144 +33,144 @@ def test_pid(): assert d.pid(m("11")) == ("00010001", Unit.NONE) def test_count(): - assert d.count("0") == (0, Unit.COUNT) - assert d.count("F") == (15, Unit.COUNT) - assert d.count("3E8") == (1000, Unit.COUNT) + assert d.count(m("00")) == (0, Unit.COUNT) + assert d.count(m("0F")) == (15, Unit.COUNT) + assert d.count(m("03E8")) == (1000, Unit.COUNT) def test_percent(): - assert d.percent("00") == (0.0, Unit.PERCENT) - assert d.percent("FF") == (100.0, Unit.PERCENT) + assert d.percent(m("00")) == (0.0, Unit.PERCENT) + assert d.percent(m("FF")) == (100.0, Unit.PERCENT) def test_percent_centered(): - assert d.percent_centered("00") == (-100.0, Unit.PERCENT) - assert d.percent_centered("80") == (0.0, Unit.PERCENT) - assert float_equals(d.percent_centered("FF"), (99.2, Unit.PERCENT)) + assert d.percent_centered(m("00")) == (-100.0, Unit.PERCENT) + assert d.percent_centered(m("80")) == (0.0, Unit.PERCENT) + assert float_equals(d.percent_centered(m("FF")), (99.2, Unit.PERCENT)) def test_temp(): - assert d.temp("00") == (-40, Unit.C) - assert d.temp("FF") == (215, Unit.C) - assert d.temp("3E8") == (960, Unit.C) + assert d.temp(m("00")) == (-40, Unit.C) + assert d.temp(m("FF")) == (215, Unit.C) + assert d.temp(m("03E8")) == (960, Unit.C) def test_catalyst_temp(): - assert d.catalyst_temp("0000") == (-40.0, Unit.C) - assert d.catalyst_temp("FFFF") == (6513.5, Unit.C) + assert d.catalyst_temp(m("0000")) == (-40.0, Unit.C) + assert d.catalyst_temp(m("FFFF")) == (6513.5, Unit.C) def test_current_centered(): - assert d.current_centered("00000000") == (-128.0, Unit.MA) - assert d.current_centered("00008000") == (0.0, Unit.MA) - assert float_equals(d.current_centered("0000FFFF"), (128.0, Unit.MA)) - assert d.current_centered("ABCD8000") == (0.0, Unit.MA) # first 2 bytes are unused (should be disregarded) + assert d.current_centered(m("00000000")) == (-128.0, Unit.MA) + assert d.current_centered(m("00008000")) == (0.0, Unit.MA) + assert float_equals(d.current_centered(m("0000FFFF")), (128.0, Unit.MA)) + assert d.current_centered(m("ABCD8000")) == (0.0, Unit.MA) # first 2 bytes are unused (should be disregarded) def test_sensor_voltage(): - assert d.sensor_voltage("0000") == (0.0, Unit.VOLT) - assert d.sensor_voltage("FFFF") == (1.275, Unit.VOLT) + assert d.sensor_voltage(m("0000")) == (0.0, Unit.VOLT) + assert d.sensor_voltage(m("FFFF")) == (1.275, Unit.VOLT) def test_sensor_voltage_big(): - assert d.sensor_voltage_big("00000000") == (0.0, Unit.VOLT) - assert float_equals(d.sensor_voltage_big("00008000"), (4.0, Unit.VOLT)) - assert d.sensor_voltage_big("0000FFFF") == (8.0, Unit.VOLT) - assert d.sensor_voltage_big("ABCD0000") == (0.0, Unit.VOLT) # first 2 bytes are unused (should be disregarded) + assert d.sensor_voltage_big(m("00000000")) == (0.0, Unit.VOLT) + assert float_equals(d.sensor_voltage_big(m("00008000")), (4.0, Unit.VOLT)) + assert d.sensor_voltage_big(m("0000FFFF")) == (8.0, Unit.VOLT) + assert d.sensor_voltage_big(m("ABCD0000")) == (0.0, Unit.VOLT) # first 2 bytes are unused (should be disregarded) def test_fuel_pressure(): - assert d.fuel_pressure("00") == (0, Unit.KPA) - assert d.fuel_pressure("80") == (384, Unit.KPA) - assert d.fuel_pressure("FF") == (765, Unit.KPA) + assert d.fuel_pressure(m("00")) == (0, Unit.KPA) + assert d.fuel_pressure(m("80")) == (384, Unit.KPA) + assert d.fuel_pressure(m("FF")) == (765, Unit.KPA) def test_pressure(): - assert d.pressure("00") == (0, Unit.KPA) - assert d.pressure("00") == (0, Unit.KPA) + assert d.pressure(m("00")) == (0, Unit.KPA) + assert d.pressure(m("00")) == (0, Unit.KPA) def test_fuel_pres_vac(): - assert d.fuel_pres_vac("0000") == (0.0, Unit.KPA) - assert d.fuel_pres_vac("FFFF") == (5177.265, Unit.KPA) + assert d.fuel_pres_vac(m("0000")) == (0.0, Unit.KPA) + assert d.fuel_pres_vac(m("FFFF")) == (5177.265, Unit.KPA) def test_fuel_pres_direct(): - assert d.fuel_pres_direct("0000") == (0, Unit.KPA) - assert d.fuel_pres_direct("FFFF") == (655350, Unit.KPA) + assert d.fuel_pres_direct(m("0000")) == (0, Unit.KPA) + assert d.fuel_pres_direct(m("FFFF")) == (655350, Unit.KPA) def test_evap_pressure(): - pass - #assert d.evap_pressure("0000") == (0.0, Unit.PA) + pass # TODO + #assert d.evap_pressure(m("0000")) == (0.0, Unit.PA) def test_abs_evap_pressure(): - assert d.abs_evap_pressure("0000") == (0, Unit.KPA) - assert d.abs_evap_pressure("FFFF") == (327.675, Unit.KPA) + assert d.abs_evap_pressure(m("0000")) == (0, Unit.KPA) + assert d.abs_evap_pressure(m("FFFF")) == (327.675, Unit.KPA) def test_evap_pressure_alt(): - assert d.evap_pressure_alt("0000") == (-32767, Unit.PA) - assert d.evap_pressure_alt("7FFF") == (0, Unit.PA) - assert d.evap_pressure_alt("FFFF") == (32768, Unit.PA) + assert d.evap_pressure_alt(m("0000")) == (-32767, Unit.PA) + assert d.evap_pressure_alt(m("7FFF")) == (0, Unit.PA) + assert d.evap_pressure_alt(m("FFFF")) == (32768, Unit.PA) def test_rpm(): - assert d.rpm("0000") == (0.0, Unit.RPM) - assert d.rpm("FFFF") == (16383.75, Unit.RPM) + assert d.rpm(m("0000")) == (0.0, Unit.RPM) + assert d.rpm(m("FFFF")) == (16383.75, Unit.RPM) def test_speed(): - assert d.speed("00") == (0, Unit.KPH) - assert d.speed("FF") == (255, Unit.KPH) + assert d.speed(m("00")) == (0, Unit.KPH) + assert d.speed(m("FF")) == (255, Unit.KPH) def test_timing_advance(): - assert d.timing_advance("00") == (-64.0, Unit.DEGREES) - assert d.timing_advance("FF") == (63.5, Unit.DEGREES) + assert d.timing_advance(m("00")) == (-64.0, Unit.DEGREES) + assert d.timing_advance(m("FF")) == (63.5, Unit.DEGREES) def test_inject_timing(): - assert d.inject_timing("0000") == (-210, Unit.DEGREES) - assert float_equals(d.inject_timing("FFFF"), (302, Unit.DEGREES)) + assert d.inject_timing(m("0000")) == (-210, Unit.DEGREES) + assert float_equals(d.inject_timing(m("FFFF")), (302, Unit.DEGREES)) def test_maf(): - assert d.maf("0000") == (0.0, Unit.GPS) - assert d.maf("FFFF") == (655.35, Unit.GPS) + assert d.maf(m("0000")) == (0.0, Unit.GPS) + assert d.maf(m("FFFF")) == (655.35, Unit.GPS) def test_max_maf(): - assert d.max_maf("00000000") == (0, Unit.GPS) - assert d.max_maf("FF000000") == (2550, Unit.GPS) - assert d.max_maf("00ABCDEF") == (0, Unit.GPS) # last 3 bytes are unused (should be disregarded) + assert d.max_maf(m("00000000")) == (0, Unit.GPS) + assert d.max_maf(m("FF000000")) == (2550, Unit.GPS) + assert d.max_maf(m("00ABCDEF")) == (0, Unit.GPS) # last 3 bytes are unused (should be disregarded) def test_seconds(): - assert d.seconds("0000") == (0, Unit.SEC) - assert d.seconds("FFFF") == (65535, Unit.SEC) + assert d.seconds(m("0000")) == (0, Unit.SEC) + assert d.seconds(m("FFFF")) == (65535, Unit.SEC) def test_minutes(): - assert d.minutes("0000") == (0, Unit.MIN) - assert d.minutes("FFFF") == (65535, Unit.MIN) + assert d.minutes(m("0000")) == (0, Unit.MIN) + assert d.minutes(m("FFFF")) == (65535, Unit.MIN) def test_distance(): - assert d.distance("0000") == (0, Unit.KM) - assert d.distance("FFFF") == (65535, Unit.KM) + assert d.distance(m("0000")) == (0, Unit.KM) + assert d.distance(m("FFFF")) == (65535, Unit.KM) def test_fuel_rate(): - assert d.fuel_rate("0000") == (0.0, Unit.LPH) - assert d.fuel_rate("FFFF") == (3276.75, Unit.LPH) + assert d.fuel_rate(m("0000")) == (0.0, Unit.LPH) + assert d.fuel_rate(m("FFFF")) == (3276.75, Unit.LPH) def test_fuel_status(): - assert d.fuel_status("0100") == ("Open loop due to insufficient engine temperature", Unit.NONE) - assert d.fuel_status("0800") == ("Open loop due to system failure", Unit.NONE) - assert d.fuel_status("0300") == (None, Unit.NONE) + assert d.fuel_status(m("0100")) == ("Open loop due to insufficient engine temperature", Unit.NONE) + assert d.fuel_status(m("0800")) == ("Open loop due to system failure", Unit.NONE) + assert d.fuel_status(m("0300")) == (None, Unit.NONE) def test_air_status(): - assert d.air_status("01") == ("Upstream", Unit.NONE) - assert d.air_status("08") == ("Pump commanded on for diagnostics", Unit.NONE) - assert d.air_status("03") == (None, Unit.NONE) + assert d.air_status(m("01")) == ("Upstream", Unit.NONE) + assert d.air_status(m("08")) == ("Pump commanded on for diagnostics", Unit.NONE) + assert d.air_status(m("03")) == (None, Unit.NONE) def test_dtc(): - assert d.dtc("0104") == ([ + assert d.dtc(m("0104")) == ([ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ], Unit.NONE) # multiple codes - assert d.dtc("010480034123") == ([ + assert d.dtc(m("010480034123")) == ([ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ("B0003", "Unknown error code"), ("C0123", "Unknown error code"), ], Unit.NONE) # invalid code lengths are dropped - assert d.dtc("01048003412") == ([ + assert d.dtc(m("0104800341")) == ([ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ("B0003", "Unknown error code"), ], Unit.NONE) # 0000 codes are dropped - assert d.dtc("000001040000") == ([ + assert d.dtc(m("000001040000")) == ([ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ], Unit.NONE) From 94d47897f459c4a5a74cfd963ed77c846a47a032 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jan 2016 01:57:40 -0500 Subject: [PATCH 294/569] test multiple messages in the DTC decoder --- obd/decoders.py | 1 + tests/test_decoders.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/obd/decoders.py b/obd/decoders.py index bda9cb76..7080ea27 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -382,6 +382,7 @@ def single_dtc(_bytes): return dtc + def dtc(messages): """ converts a frame of 2-byte DTCs into a list of DTCs """ codes = [] diff --git a/tests/test_decoders.py b/tests/test_decoders.py index edb083d4..4e7d64c9 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -174,3 +174,9 @@ def test_dtc(): assert d.dtc(m("000001040000")) == ([ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ], Unit.NONE) + + # test multiple messages + assert d.dtc(m("0104") + m("8003") + m("0000")) == ([ + ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), + ("B0003", "Unknown error code"), + ], Unit.NONE) From 6eeaa0ed45cac3b0c3127f76c718f10f51cde98b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jan 2016 14:09:47 -0500 Subject: [PATCH 295/569] use dict.get() for lookups with default values --- obd/protocols/protocol.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index e8b0dad9..614d220f 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -100,7 +100,7 @@ def __eq__(self, other): Protocol objects are factories for Frame and Message objects. They are largely stateless, with the exception of an ECU tagging system, which -are initialized by passing the response to an "0100" command. +is initialized by passing the response to an "0100" command. Protocols are __called__ with a list of string responses, and return a list of Messages. @@ -196,7 +196,7 @@ def __call__(self, lines): # subclass function to assemble frames into Messages if self.parse_message(message): # mark with the appropriate ECU ID - message.ecu = self.lookup_ecu(ecu) + message.ecu = self.ecu_map.get(ecu, ECU.UNKNOWN) messages.append(message) # ----------- handle invalid lines (probably from the ELM) ----------- @@ -209,13 +209,6 @@ def __call__(self, lines): return messages - def lookup_ecu(self, tx_id): - if tx_id in self.ecu_map: - return self.ecu_map[tx_id] - else: - return ECU.UNKNOWN - - def populate_ecu_map(self, messages): """ Given a list of messages from different ECUS, From 4b652d00bc975855dbc28b94dd0ee737cdc90ec0 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jan 2016 14:13:25 -0500 Subject: [PATCH 296/569] updated copyright --- obd/OBDCommand.py | 2 +- obd/OBDResponse.py | 2 +- obd/__init__.py | 2 +- obd/async.py | 2 +- obd/codes.py | 2 +- obd/commands.py | 2 +- obd/debug.py | 2 +- obd/decoders.py | 2 +- obd/elm327.py | 2 +- obd/obd.py | 2 +- obd/protocols/__init__.py | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 86fce4b5..ffee906d 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -6,7 +6,7 @@ # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # # Copyright 2009 Peter J. Creath # -# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2016 Brendan Whitfield (brendan-w.com) # # # ######################################################################## # # diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py index fd7309ca..59148f72 100644 --- a/obd/OBDResponse.py +++ b/obd/OBDResponse.py @@ -6,7 +6,7 @@ # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # # Copyright 2009 Peter J. Creath # -# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2016 Brendan Whitfield (brendan-w.com) # # # ######################################################################## # # diff --git a/obd/__init__.py b/obd/__init__.py index 89ab29c6..78294b4e 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -13,7 +13,7 @@ # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # # Copyright 2009 Peter J. Creath # -# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2016 Brendan Whitfield (brendan-w.com) # # # ######################################################################## # # diff --git a/obd/async.py b/obd/async.py index 99783ec9..28be7729 100644 --- a/obd/async.py +++ b/obd/async.py @@ -6,7 +6,7 @@ # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # # Copyright 2009 Peter J. Creath # -# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2016 Brendan Whitfield (brendan-w.com) # # # ######################################################################## # # diff --git a/obd/codes.py b/obd/codes.py index 2b6d277e..8278d922 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -6,7 +6,7 @@ # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # # Copyright 2009 Peter J. Creath # -# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2016 Brendan Whitfield (brendan-w.com) # # # ######################################################################## # # diff --git a/obd/commands.py b/obd/commands.py index 483bb779..3b359d7b 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -6,7 +6,7 @@ # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # # Copyright 2009 Peter J. Creath # -# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2016 Brendan Whitfield (brendan-w.com) # # # ######################################################################## # # diff --git a/obd/debug.py b/obd/debug.py index 336ae418..06105697 100644 --- a/obd/debug.py +++ b/obd/debug.py @@ -6,7 +6,7 @@ # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # # Copyright 2009 Peter J. Creath # -# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2016 Brendan Whitfield (brendan-w.com) # # # ######################################################################## # # diff --git a/obd/decoders.py b/obd/decoders.py index 7080ea27..c6a99587 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -6,7 +6,7 @@ # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # # Copyright 2009 Peter J. Creath # -# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2016 Brendan Whitfield (brendan-w.com) # # # ######################################################################## # # diff --git a/obd/elm327.py b/obd/elm327.py index a9d8fcdd..78e6e8e0 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -6,7 +6,7 @@ # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # # Copyright 2009 Peter J. Creath # -# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2016 Brendan Whitfield (brendan-w.com) # # # ######################################################################## # # diff --git a/obd/obd.py b/obd/obd.py index f6b531e2..0678de8e 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -6,7 +6,7 @@ # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # # Copyright 2009 Peter J. Creath # -# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2016 Brendan Whitfield (brendan-w.com) # # # ######################################################################## # # diff --git a/obd/protocols/__init__.py b/obd/protocols/__init__.py index 5460dcf4..9860372d 100644 --- a/obd/protocols/__init__.py +++ b/obd/protocols/__init__.py @@ -6,7 +6,7 @@ # Copyright 2004 Donour Sizemore (donour@uchicago.edu) # # Copyright 2009 Secons Ltd. (www.obdtester.com) # # Copyright 2009 Peter J. Creath # -# Copyright 2015 Brendan Whitfield (bcw7044@rit.edu) # +# Copyright 2016 Brendan Whitfield (brendan-w.com) # # # ######################################################################## # # From 3524fa4a6a471ea6863b235fb86bd01b92970867 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jan 2016 14:20:13 -0500 Subject: [PATCH 297/569] comments for protocol constants --- obd/protocols/protocol.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 614d220f..7d71e8a8 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -111,9 +111,10 @@ class Protocol(object): # override in subclass for each protocol - ELM_NAME = "" - ELM_ID = "" + ELM_NAME = "" # the ELM's name for this protocol (ie, "SAE J1939 (CAN 29/250)") + ELM_ID = "" # the ELM's ID for this protocol (ie, "A") + # the TX_IDs of known ECUs TX_ID_ENGINE = None From 7952a5c4bf8150e11871b665934729621ac7b57c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jan 2016 14:38:43 -0500 Subject: [PATCH 298/569] passing actual bytearrays, using unhexlify --- obd/OBDCommand.py | 2 +- obd/protocols/protocol.py | 6 +++--- obd/protocols/protocol_can.py | 3 ++- obd/protocols/protocol_legacy.py | 3 ++- obd/utils.py | 4 ---- tests/test_OBDCommand.py | 6 +++--- tests/test_protocol_can.py | 2 +- tests/test_protocol_legacy.py | 2 +- 8 files changed, 13 insertions(+), 15 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index ffee906d..46478536 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -105,7 +105,7 @@ def __constrain_message_data(self, message): message.data = message.data[:self.bytes] else: # pad the right with zeros - message.data += ([0] * (self.bytes - len(message.data))) + message.data += (b'\x00' * (self.bytes - len(message.data))) def __str__(self): diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 7d71e8a8..180927d7 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -29,7 +29,7 @@ # # ######################################################################## -from obd.utils import ascii_to_bytes, isHex, numBitsSet +from obd.utils import isHex, numBitsSet from obd.debug import debug @@ -56,7 +56,7 @@ class Frame(object): """ represents a single parsed line of OBD output """ def __init__(self, raw): self.raw = raw - self.data = [] + self.data = b'' self.priority = None self.addr_mode = None self.rx_id = None @@ -71,7 +71,7 @@ class Message(object): def __init__(self, frames): self.frames = frames self.ecu = ECU.UNKNOWN - self.data = [] + self.data = b'' @property def tx_id(self): diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 8abe7bc4..67859d5a 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -29,6 +29,7 @@ # # ######################################################################## +from binascii import unhexlify from obd.utils import contiguous from .protocol import * @@ -63,7 +64,7 @@ def parse_frame(self, frame): if self.id_bits == 11: raw = "00000" + raw - raw_bytes = ascii_to_bytes(raw) + raw_bytes = unhexlify(raw) # check for valid size diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 0e28b2b8..4dc989ba 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -29,6 +29,7 @@ # # ######################################################################## +from binascii import unhexlify from obd.utils import contiguous from .protocol import * @@ -46,7 +47,7 @@ def parse_frame(self, frame): raw = frame.raw - raw_bytes = ascii_to_bytes(raw) + raw_bytes = unhexlify(raw) if len(raw_bytes) < 6: debug("Dropped frame for being too short") diff --git a/obd/utils.py b/obd/utils.py index 54d620da..757de1ba 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -86,10 +86,6 @@ def bytes_to_hex(bs): h += ("0" * (2 - len(bh))) + bh return h -def ascii_to_bytes(a): - """ converts a string of hex to an array of integer byte values """ - return [ unhex(a[i:i+2]) for i in range(0, len(a), 2) ] - def bitstring(_hex, bits=None): b = bin(unhex(_hex))[2:] if bits is not None: diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index 09751b32..e8f35b42 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -62,17 +62,17 @@ def test_call(): # valid response size cmd = OBDCommand("", "", "0123", 4, decode_raw, ECU.ENGINE) r = cmd(messages) - assert r.value == [0xBE, 0x1F, 0xB8, 0x11] + assert r.value == b'\xBE\x1F\xB8\x11' # response too short (pad) cmd = OBDCommand("", "", "0123", 5, decode_raw, ECU.ENGINE) r = cmd(messages) - assert r.value == [0xBE, 0x1F, 0xB8, 0x11, 0x00] + assert r.value == b'\xBE\x1F\xB8\x11\x00' # response too long (clip) cmd = OBDCommand("", "", "0123", 3, decode_raw, ECU.ENGINE) r = cmd(messages) - assert r.value == [0xBE, 0x1F, 0xB8] + assert r.value == b'\xBE\x1F\xB8' diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index 95eef542..55a5f5dc 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -23,7 +23,7 @@ def check_message(m, num_frames, tx_id, data): """ generic test for correct message values """ assert len(m.frames) == num_frames assert m.tx_id == tx_id - assert m.data == data + assert m.data == bytes(data) diff --git a/tests/test_protocol_legacy.py b/tests/test_protocol_legacy.py index 142e4b67..7bdf8c25 100644 --- a/tests/test_protocol_legacy.py +++ b/tests/test_protocol_legacy.py @@ -20,7 +20,7 @@ def check_message(m, n_frames, tx_id, data): """ generic test for correct message values """ assert len(m.frames) == n_frames assert m.tx_id == tx_id - assert m.data == data + assert m.data == bytes(data) def test_single_frame(): From be83cd917ac78b1439202ec3cf21acfb57ae218b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jan 2016 15:02:16 -0500 Subject: [PATCH 299/569] added Message.hex() function for backwards compatability --- obd/protocols/protocol.py | 4 ++++ tests/test_protocol.py | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 180927d7..e9c71f95 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -29,6 +29,7 @@ # # ######################################################################## +from binascii import hexlify from obd.utils import isHex, numBitsSet from obd.debug import debug @@ -80,6 +81,9 @@ def tx_id(self): else: return self.frames[0].tx_id + def hex(self): + return hexlify(self.data) + def parsed(self): """ boolean for whether this message was successfully parsed """ return bool(self.data) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 624c4bd4..a7f95a1a 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -1,5 +1,6 @@ import random +from obd.utils import unhex from obd.protocols import * from obd.protocols.protocol import Frame, Message @@ -49,6 +50,16 @@ def test_message(): assert Message([]).tx_id == None # if no frames are given, then we can't report a tx_id +def test_message_hex(): + message = Message([]) + message.data = b'\x00\x01\x02' + + assert message.hex() == b'000102' + assert unhex(message.hex()[0:2]) == 0x00 + assert unhex(message.hex()[2:4]) == 0x01 + assert unhex(message.hex()[4:6]) == 0x02 + assert unhex(message.hex()) == 0x000102 + def test_populate_ecu_map(): # parse from messages From 90773f395653154f96b6379e06c9553a9d04187f Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jan 2016 16:07:40 -0500 Subject: [PATCH 300/569] updated docs for custom commands --- docs/Custom Commands.md | 92 ++++++++++++++++++++++++++++++----------- obd/OBDCommand.py | 4 +- 2 files changed, 70 insertions(+), 26 deletions(-) diff --git a/docs/Custom Commands.md b/docs/Custom Commands.md index a5952138..51cf80fc 100644 --- a/docs/Custom Commands.md +++ b/docs/Custom Commands.md @@ -1,42 +1,86 @@ If the command you need is not in python-OBDs tables, you can create a new `OBDCommand` object. The constructor accepts the following arguments (each will become a property). -| Argument | Type | Description | -|----------------------|----------|--------------------------------------------------------------------------| -| name | string | (human readability only) | -| desc | string | (human readability only) | -| mode | string | OBD mode (hex) | -| pid | string | OBD PID (hex) | -| bytes | int | Number of bytes expected in response | -| decoder | callable | Function used for decoding the hex response | -| supported (optional) | bool | Flag to prevent the sending of unsupported commands (`False` by default) | - -*When the command is sent, the `mode` and `pid` properties are simply concatenated. For unusual codes that don't follow the `mode + pid` structure, feel free to use just one, while setting the other to an empty string.* +| Argument | Type | Description | +|----------------------|----------|----------------------------------------------------------------------------| +| name | string | (human readability only) | +| desc | string | (human readability only) | +| command | string | OBD command in hex (typically mode + PID | +| bytes | int | Number of bytes expected in response | +| decoder | callable | Function used for decoding the hex response | +| ecu (optional) | ECU | ID of the ECU this command should listen to (`ECU.ALL` by default) | +| fast (optional) | bool | Allows python-OBD to alter this command for efficieny (`False` by default) | +| supported (optional) | bool | Flag to prevent the sending of unsupported commands (`False` by default) | -The `decoder` argument is a function of following form. + +Example +------- ```python - def (_hex): - ... - return (, ) +from obd import OBDCommand +from obd.protocols import ECU +from obd.utils import bytes_to_int + +def rpm(messages): + d = messages[0].data + v = bytes_to_int(d) / 4.0 # helper function for converting byte arrays to ints + return (v, Unit.RPM) + +c = OBDCommand("RPM", \ # name + "Engine RPM", \ # description + "010C", \ # command + 2, \ # number of return bytes to expect + rpm, \ # decoding function + ECU.ENGINE, \ # (optional) ECU filter + True, \ # (optional) allow a "01" to be added for speed + True), # (optional) supported by deafult ``` -The `_hex` argument is the data recieved from the car, and is guaranteed to be the size of the `bytes` property specified in the OBDCommand. +Here are some details on the less intuitive fields: -For example: +
+ +--- + +### OBDCommand.decoder + +The `decoder` argument is a function of following form. ```python -from obd import OBDCommand -from obd.utils import unhex +def (): + ... + return (, ) +``` -def rpm(_hex): - v = unhex(_hex) # helper function to convert hex to int - v = v / 4.0 - return (v, obd.Unit.RPM) +Decoders are given a list of `Message` objects as an argument. If your decoder is called, this list is garaunteed to have at least one message object. Each `Message` object has a `data` property, which holds a parsed byte array, and is also garauteed to have the number of bytes specified by the command. + +*NOTE: If you are transitioning from an older version of Python-OBD (where decoders were given raw hex strings as arguments), you can use the `Message.hex()` function as a patch.* + +```python +def (messages): + _hex = messages[0].hex() + ... + return (, ) -c = OBDCommand("RPM", "Engine RPM", "01", "0C", 2, rpm) ``` --- +### OBDCommand.ecu + +The `ecu` argument is a constant used to filter incoming messages. Some commands may listen to multiple ECUs (such as DTC decoders), where others may only be concerned with the engine (such as RPM). Currently, python-OBD can only distinguish the engine, but this list may be expanded over time: + +- `ECU.ALL` +- `ECU.ALL_KNOWN` +- `ECU.UNKNOWN` +- `ECU.ENGINE` + +--- + +### OBDCommand.fast + +The `fast` argument tells python-OBD whether it is safe to append a `"01"` to the end of the command. This will instruct the adapter to return the first response it recieves, rather than waiting for more (and eventually reaching a timeout). This can speed up requests significantly, and is enabled for most of python-OBDs internal commands. However, for unusual commands, it is safest to leave this disabled. + +--- +
diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 46478536..5226a0b3 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -40,7 +40,7 @@ def __init__(self, name, desc, command, - returnBytes, + _bytes, decoder, ecu=ECU.ALL, fast=False, @@ -48,7 +48,7 @@ def __init__(self, self.name = name # human readable name (also used as key in commands dict) self.desc = desc # human readable description self.command = command # command string - self.bytes = returnBytes # number of bytes expected in return + self.bytes = _bytes # number of bytes expected in return self.decode = decoder # decoding function self.ecu = ecu # ECU ID from which this command expects messages from self.fast = fast # can an extra digit be added to the end of the command? (to make the ELM return early) From 39ccc16378528c83123852082a383fe455c549e7 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 25 Jan 2016 16:47:59 -0500 Subject: [PATCH 301/569] fixed some comments --- obd/obd.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/obd/obd.py b/obd/obd.py index 0678de8e..567747b2 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -50,7 +50,7 @@ def __init__(self, portstr=None, baudrate=38400, protocol=None, fast=True): self.port = None self.supported_commands = [] self.fast = fast - self.__last_command = "" # used for + self.__last_command = "" # used for running the previous command with a CR debug("========================== python-OBD (v%s) ==========================" % __version__) self.__connect(portstr, baudrate, protocol) # initialize by connecting and loading sensors @@ -256,6 +256,7 @@ def __build_command_string(self, cmd): """ assembles the appropriate command string """ cmd_string = cmd.command + # only wait for as many ECUs as we've seen if self.fast and cmd.fast: cmd_string += str(len(self.port.ecus())) From 768a3708764f6fc5c8769b65897f56b19255f636 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 25 Jan 2016 16:54:29 -0500 Subject: [PATCH 302/569] better implementation of num_bits_set() --- obd/elm327.py | 2 +- obd/protocols/protocol.py | 4 ++-- obd/utils.py | 10 ++-------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 78e6e8e0..53df9911 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -33,7 +33,7 @@ import serial import time from .protocols import * -from .utils import OBDStatus, numBitsSet +from .utils import OBDStatus from .debug import debug diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index e9c71f95..47d4036f 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -30,7 +30,7 @@ ######################################################################## from binascii import hexlify -from obd.utils import isHex, numBitsSet +from obd.utils import isHex, num_bits_set from obd.debug import debug @@ -255,7 +255,7 @@ def populate_ecu_map(self, messages): tx_id = None for message in messages: - bits = sum([numBitsSet(b) for b in message.data]) + bits = sum([num_bits_set(b) for b in message.data]) if bits > best: best = bits diff --git a/obd/utils.py b/obd/utils.py index 757de1ba..2d86b3aa 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -47,14 +47,8 @@ class OBDStatus: -def numBitsSet(n): - # TODO: there must be a better way to do this... - total = 0 - ref = 1 - for b in range(8): - total += int(bool(n & ref)) - ref = ref << 1 - return total +def num_bits_set(n): + return bin(n).count("1") def unhex(_hex): _hex = "0" if _hex == "" else _hex From 3ddba33ade45f1a94d59b6862343568793cedb51 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 25 Jan 2016 19:07:26 -0500 Subject: [PATCH 303/569] touched up the docs with new info on OBD constructor --- docs/Connections.md | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/Connections.md b/docs/Connections.md index 47ebdd74..239f5c88 100644 --- a/docs/Connections.md +++ b/docs/Connections.md @@ -17,13 +17,31 @@ print ports # ['/dev/ttyUSB0', '/dev/ttyUSB1'] connection = obd.OBD(ports[0]) # connect to the first port in the list ``` + +
+ +### OBD(portstr=None, baudrate=38400, protocol=None, fast=True): + +`portstr`: The UNIX device file or Windows COM Port for your adapter. The default value (`None`) will auto select a port. + +`baudrate`: The baudrate at which to set the serial connection. This can vary from adapter to adapter. Typical values are: 9600, 38400, 19200, 57600, 115200 + +`protocol`: Forces python-OBD to use the given protocol when communicating with the adapter. See `protocol_id()` for possible values. The default value (`None`) will auto select a protocol. + +`fast`: Allows commands to be optimized before being sent to the car. Python-OBD currently makes two such optimizations: + +- Sends carriage returns to repeat the previous command. +- Appends a response limit to the end of the command, telling the adapter to return after it receives *N* responses (rather than waiting and eventually timing out). This feature can be enabled and disabled for individual commands. + +Disabling fast mode will guarantee that python-OBD outputs the unaltered command for every request. +
--- ### query(command, force=False) -Sends an `OBDCommand` to the car, and returns a `OBDResponse` object. This function will block until a response is recieved from the car. This function will also check whether the given command is supported by your car. If a command is not marked as supported, it will not be sent to the car, and an empty `Response` will be returned. To force an unsupported command to be sent, there is an optional `force` parameter for your convenience. +Sends an `OBDCommand` to the car, and returns a `OBDResponse` object. This function will block until a response is received from the car. This function will also check whether the given command is supported by your car. If a command is not marked as supported, it will not be sent to the car, and an empty `Response` will be returned. To force an unsupported command to be sent, there is an optional `force` parameter for your convenience. *For non-blocking querying, see [Async Querying](Async Connections.md)* @@ -49,7 +67,7 @@ OBDStatus.NOT_CONNECTED # "Not Connected" # successful communication with the ELM327 adapter OBDStatus.ELM_CONNECTED # "ELM Connected" -# successful communication with the vehicle +# successful communication with the ELM327 and the vehicle OBDStatus.CAR_CONNECTED # "Car Connected" ``` @@ -88,7 +106,7 @@ Returns a boolean for whether a command is supported by both the car and python- ### protocol_id() ### protocol_name() -Both functions return string names for the protocol currently being used by the adapter. Protocol *ID's* are the short names used by your adapter, whereas protocol *names* are the human-readable versions. The `protocol_id()` function is a good way to lookup which value to pass in the `protocol` field of the OBD constructor (though, this is mainly for advanced usage). These function do not make any serial requests. When no connection has been made, these functions will return empty strings. The possible values are: +Both functions return string names for the protocol currently being used by the adapter. Protocol *ID's* are the short values used by your adapter, whereas protocol *names* are the human-readable versions. The `protocol_id()` function is a good way to lookup which value to pass in the `protocol` field of the OBD constructor (though, this is mainly for advanced usage). These functions do not make any serial requests. When no connection has been made, these functions will return empty strings. The possible values are: |ID | Name | |---|--------------------------| From b716fa4814f3e49524bad6c78cfc6e73b1f42062 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 25 Jan 2016 19:37:12 -0500 Subject: [PATCH 304/569] added a high level description to the welcome page --- docs/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index ef0afeaf..05282dce 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,7 +12,7 @@ Install the latest release from pypi: $ pip install obd ``` -If you are using a bluetooth adapter on Debian-based linux, you will need to install the following packages: +*Note: If you are using a Bluetooth adapter on Linux, you may also need to install and configure your Bluetooth stack. On Debian-based systems, this usually means installing the following packages:* ```shell $ sudo apt-get install bluetooth bluez-utils blueman @@ -35,6 +35,8 @@ print(response.value) print(response.unit) ``` +OBD connections operate in a request-reply fashion. To retrieve data from the car, you must send commands that query for the data you want (e.g. RPM, Vehicle speed, etc). In python-OBD, this is done with the `query()` function. The commands themselves are represented as objects, and can be looked up by name or value in `obd.commands`. The `query()` function will return a response object with parsed data in its `value` and `unit` properties. +
# License From 446355a0ae8d7be23ab661f0090b6c87775df47c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 25 Jan 2016 21:30:54 -0500 Subject: [PATCH 305/569] started writing tests for main API --- tests/test_OBD.py | 130 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 124 insertions(+), 6 deletions(-) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index f9cfc45d..4722a854 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -1,17 +1,138 @@ +""" + Tests for the API layer +""" + import obd +from obd import Unit +from obd import ECU +from obd.protocols.protocol import Message from obd.utils import OBDStatus -from obd.OBDResponse import OBDResponse from obd.OBDCommand import OBDCommand from obd.decoders import noop -from obd.protocols import SAE_J1850_PWM + + + +class FakeELM: + """ + Fake ELM327 driver class for intercepting the commands from the API + """ + + def __init__(self, portname, baudrate, protocol): + self.portname = portname + self.baudrate = baudrate + self.protocol = protocol + self.last_command = None + + def port_name(self): + return self.portname + + def status(self): + return OBDStatus.CAR_CONNECTED + + def ecus(self): + return [ ECU.ENGINE, ECU.UNKNOWN ] + + def protocol_name(self): + return "ISO 15765-4 (CAN 11/500)" + + def protocol_id(self): + return "6" + + def close(self): + pass + + def send_and_parse(self, cmd): + # stow this, so we can check that the API made the right request + print(cmd) + self.last_command = cmd + + # all commands succeed + message = Message([]) + message.data = b'response data' + message.ecu = ECU.ENGINE # picked engine so that simple commands like RPM will work + return [ message ] + + def _test_last_command(self, expected): + r = self.last_command == expected + print(self.last_command) + self.last_command = None + return r + + +# a toy command to test with + +def decoder(messages): + return (messages[0].data, Unit.NONE) + +command = OBDCommand("Test_Command", "A test command", "0123456789ABCDEF", 0, decoder, ECU.ALL, True, True) + + + def test_is_connected(): o = obd.OBD("/dev/null") assert not o.is_connected() + assert not o.supports(obd.commands.RPM) + + # our fake ELM class always returns success for connections + o.port = FakeELM("/dev/null", 34800, None) + assert o.is_connected() + + +def test_supports(): + o = obd.OBD("/dev/null") + + # since we haven't actually connected, + # no commands should be marked as supported + assert not o.supports(obd.commands.RPM) + obd.commands.RPM.supported = True + assert o.supports(obd.commands.RPM) + + # commands that aren't in python-OBD's tables are unsupported by default + assert not o.supports(command) + + +def test_force(): + o = obd.OBD("/dev/null", fast=False) # disable the trailing response count byte + o.port = FakeELM("/dev/null", 34800, None) + + # a command marked as unsupported + obd.commands.RPM.supported = False + + r = o.query(obd.commands.RPM) + assert r.is_null() + assert o.port._test_last_command(None) + + r = o.query(obd.commands.RPM, force=True) + assert not r.is_null() + assert o.port._test_last_command(obd.commands.RPM.command) + + # a command that isn't in python-OBD's tables + r = o.query(command) + assert r.is_null() + assert o.port._test_last_command(None) + + r = o.query(command, force=True) + assert o.port._test_last_command(command.command) + + + +def test_fast(): + o = obd.OBD("/dev/null", fast=False) + o.port = FakeELM("/dev/null", 34800, None) + + + assert command.fast + o.query(command, force=True) # force since this command isn't in the tables + # assert o.port._test_last_command(command.command) + + + + + - # todo """ # TODO: rewrite for new protocol architecture @@ -98,6 +219,3 @@ def write(cmd): assert r.is_null() ''' """ - -def test_load_commands(): - pass From c8562c73b48eba5d14092af552956d73760b8e0a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 25 Jan 2016 21:39:31 -0500 Subject: [PATCH 306/569] changed noop to drop in favor of noop passing raw values --- obd/commands.py | 22 +++++++++++----------- obd/decoders.py | 10 ++++++++-- tests/test_OBD.py | 13 ++++++++----- tests/test_OBDCommand.py | 12 +++--------- tests/test_decoders.py | 5 ++++- 5 files changed, 34 insertions(+), 28 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 3b359d7b..9bef1d0c 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -50,7 +50,7 @@ # name description cmd bytes decoder ECU fast OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "0100", 4, pid, ECU.ENGINE, True, True), # the first PID getter is assumed to be supported OBDCommand("STATUS" , "Status since DTCs cleared" , "0101", 4, status, ECU.ENGINE, True), - OBDCommand("FREEZE_DTC" , "Freeze DTC" , "0102", 2, noop, ECU.ENGINE, True), + OBDCommand("FREEZE_DTC" , "Freeze DTC" , "0102", 2, drop, ECU.ENGINE, True), OBDCommand("FUEL_STATUS" , "Fuel System Status" , "0103", 2, fuel_status, ECU.ENGINE, True), OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , "0104", 1, percent, ECU.ENGINE, True), OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "0105", 1, temp, ECU.ENGINE, True), @@ -67,7 +67,7 @@ OBDCommand("MAF" , "Air Flow Rate (MAF)" , "0110", 2, maf, ECU.ENGINE, True), OBDCommand("THROTTLE_POS" , "Throttle Position" , "0111", 1, percent, ECU.ENGINE, True), OBDCommand("AIR_STATUS" , "Secondary Air Status" , "0112", 1, air_status, ECU.ENGINE, True), - OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "0113", 1, noop, ECU.ENGINE, True), + OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "0113", 1, drop, ECU.ENGINE, True), OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , "0114", 2, sensor_voltage, ECU.ENGINE, True), OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , "0115", 2, sensor_voltage, ECU.ENGINE, True), OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , "0116", 2, sensor_voltage, ECU.ENGINE, True), @@ -77,8 +77,8 @@ OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , "011A", 2, sensor_voltage, ECU.ENGINE, True), OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , "011B", 2, sensor_voltage, ECU.ENGINE, True), OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "011C", 1, obd_compliance, ECU.ENGINE, True), - OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "011D", 1, noop, ECU.ENGINE, True), - OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "011E", 1, noop, ECU.ENGINE, True), + OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "011D", 1, drop, ECU.ENGINE, True), + OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "011E", 1, drop, ECU.ENGINE, True), OBDCommand("RUN_TIME" , "Engine Run Time" , "011F", 2, seconds, ECU.ENGINE, True), # name description cmd bytes decoder ECU fast @@ -117,10 +117,10 @@ # name description cmd bytes decoder ECU fast OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "0140", 4, pid, ECU.ENGINE, True), - OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "0141", 4, noop, ECU.ENGINE, True), - OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "0142", 2, noop, ECU.ENGINE, True), - OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "0143", 2, noop, ECU.ENGINE, True), - OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "0144", 2, noop, ECU.ENGINE, True), + OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "0141", 4, drop, ECU.ENGINE, True), + OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "0142", 2, drop, ECU.ENGINE, True), + OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "0143", 2, drop, ECU.ENGINE, True), + OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "0144", 2, drop, ECU.ENGINE, True), OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , "0145", 1, percent, ECU.ENGINE, True), OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , "0146", 1, temp, ECU.ENGINE, True), OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , "0147", 1, percent, ECU.ENGINE, True), @@ -131,7 +131,7 @@ OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , "014C", 1, percent, ECU.ENGINE, True), OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , "014D", 2, minutes, ECU.ENGINE, True), OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , "014E", 2, minutes, ECU.ENGINE, True), - OBDCommand("MAX_VALUES" , "Various Max values" , "014F", 4, noop, ECU.ENGINE, True), # todo: decode this + OBDCommand("MAX_VALUES" , "Various Max values" , "014F", 4, drop, ECU.ENGINE, True), # todo: decode this OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , "0150", 4, max_maf, ECU.ENGINE, True), OBDCommand("FUEL_TYPE" , "Fuel Type" , "0151", 1, fuel_type, ECU.ENGINE, True), OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , "0152", 1, percent, ECU.ENGINE, True), @@ -147,7 +147,7 @@ OBDCommand("OIL_TEMP" , "Engine oil temperature" , "015C", 1, temp, ECU.ENGINE, True), OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , "015D", 2, inject_timing, ECU.ENGINE, True), OBDCommand("FUEL_RATE" , "Engine fuel rate" , "015E", 2, fuel_rate, ECU.ENGINE, True), - OBDCommand("EMISSION_REQ" , "Designed emission requirements" , "015F", 1, noop, ECU.ENGINE, True), + OBDCommand("EMISSION_REQ" , "Designed emission requirements" , "015F", 1, drop, ECU.ENGINE, True), ] @@ -168,7 +168,7 @@ __mode4__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", 0, noop, ECU.ALL, False, True), + OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", 0, drop, ECU.ALL, False, True), ] __mode7__ = [ diff --git a/obd/decoders.py b/obd/decoders.py index c6a99587..22047ff8 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -45,10 +45,16 @@ def (): ''' -# hex in, hex out -def noop(messages): +# drop all messages, return None +def drop(messages): return (None, Unit.NONE) + +# data in, data out +def noop(messages): + return (messages[0].data, Unit.NONE) + + # hex in, bitstring out def pid(messages): d = messages[0].data diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 4722a854..c7626fab 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -61,11 +61,14 @@ def _test_last_command(self, expected): # a toy command to test with - -def decoder(messages): - return (messages[0].data, Unit.NONE) - -command = OBDCommand("Test_Command", "A test command", "0123456789ABCDEF", 0, decoder, ECU.ALL, True, True) +command = OBDCommand("Test_Command", \ + "A test command", \ + "0123456789ABCDEF", \ + 0, \ + noop, \ + ECU.ALL, \ + True, \ + True) diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index e8f35b42..4e5498d8 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -6,12 +6,6 @@ -def decode_raw(messages): - """ a passiver decoder """ - return (messages[0].data, Unit.NONE) - - - def test_constructor(): # default constructor @@ -60,17 +54,17 @@ def test_call(): print(messages[0].data) # valid response size - cmd = OBDCommand("", "", "0123", 4, decode_raw, ECU.ENGINE) + cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) r = cmd(messages) assert r.value == b'\xBE\x1F\xB8\x11' # response too short (pad) - cmd = OBDCommand("", "", "0123", 5, decode_raw, ECU.ENGINE) + cmd = OBDCommand("", "", "0123", 5, noop, ECU.ENGINE) r = cmd(messages) assert r.value == b'\xBE\x1F\xB8\x11\x00' # response too long (clip) - cmd = OBDCommand("", "", "0123", 3, decode_raw, ECU.ENGINE) + cmd = OBDCommand("", "", "0123", 3, noop, ECU.ENGINE) r = cmd(messages) assert r.value == b'\xBE\x1F\xB8' diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 4e7d64c9..cdb6ec73 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -25,7 +25,10 @@ def float_equals(d1, d2): def test_noop(): - assert d.noop(m("deadbeef")) == (None, Unit.NONE) + assert d.noop(m("00010203")) == ([0, 1, 2, 3], Unit.NONE) + +def test_drop(): + assert d.drop(m("deadbeef")) == (None, Unit.NONE) def test_pid(): assert d.pid(m("00000000")) == ("00000000000000000000000000000000", Unit.NONE) From f4e1b0306f8abf64c212fb661477936aba4cea8a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 25 Jan 2016 21:48:22 -0500 Subject: [PATCH 307/569] added test for status command --- tests/test_OBD.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index c7626fab..8a3f7393 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -22,13 +22,14 @@ def __init__(self, portname, baudrate, protocol): self.portname = portname self.baudrate = baudrate self.protocol = protocol + self._status = OBDStatus.CAR_CONNECTED self.last_command = None def port_name(self): return self.portname def status(self): - return OBDStatus.CAR_CONNECTED + return self._status def ecus(self): return [ ECU.ENGINE, ECU.UNKNOWN ] @@ -77,13 +78,31 @@ def _test_last_command(self, expected): def test_is_connected(): o = obd.OBD("/dev/null") assert not o.is_connected() - assert not o.supports(obd.commands.RPM) # our fake ELM class always returns success for connections o.port = FakeELM("/dev/null", 34800, None) assert o.is_connected() +def test_status(): + """ + Make sure that the API's status() functions reports the + same values as the underlying ELM327 class. + """ + o = obd.OBD("/dev/null") + assert o.status() == OBDStatus.NOT_CONNECTED + + # we can manually set our fake ELM class to test + # the other values + o.port = FakeELM("/dev/null", 34800, None) + + o.port._status = OBDStatus.ELM_CONNECTED + assert o.status() == OBDStatus.ELM_CONNECTED + + o.port._status = OBDStatus.CAR_CONNECTED + assert o.status() == OBDStatus.CAR_CONNECTED + + def test_supports(): o = obd.OBD("/dev/null") From 90234a21a4b653a693144bba2d649d884fa7b93b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 25 Jan 2016 21:51:39 -0500 Subject: [PATCH 308/569] added test for port_name --- tests/test_OBD.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 8a3f7393..bd6fedd2 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -19,14 +19,14 @@ class FakeELM: """ def __init__(self, portname, baudrate, protocol): - self.portname = portname - self.baudrate = baudrate - self.protocol = protocol + self._portname = portname + self._baudrate = baudrate + self._protocol = protocol self._status = OBDStatus.CAR_CONNECTED - self.last_command = None + self._last_command = None def port_name(self): - return self.portname + return self._portname def status(self): return self._status @@ -46,7 +46,7 @@ def close(self): def send_and_parse(self, cmd): # stow this, so we can check that the API made the right request print(cmd) - self.last_command = cmd + self._last_command = cmd # all commands succeed message = Message([]) @@ -55,9 +55,8 @@ def send_and_parse(self, cmd): return [ message ] def _test_last_command(self, expected): - r = self.last_command == expected - print(self.last_command) - self.last_command = None + r = self._last_command == expected + self._last_command = None return r @@ -86,7 +85,7 @@ def test_is_connected(): def test_status(): """ - Make sure that the API's status() functions reports the + Make sure that the API's status() function reports the same values as the underlying ELM327 class. """ o = obd.OBD("/dev/null") @@ -116,6 +115,19 @@ def test_supports(): assert not o.supports(command) +def test_port_name(): + """ + Make sure that the API's port_name() function reports the + same values as the underlying ELM327 class. + """ + o = obd.OBD("/dev/null") + o.port = FakeELM("/dev/null", 34800, None) + assert o.port_name() == o.port._portname + + o.port = FakeELM("A different port name", 34800, None) + assert o.port_name() == o.port._portname + + def test_force(): o = obd.OBD("/dev/null", fast=False) # disable the trailing response count byte o.port = FakeELM("/dev/null", 34800, None) From 4bd55271a4fd674b2d4bb09655f320722b329701 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 25 Jan 2016 21:57:43 -0500 Subject: [PATCH 309/569] added protocol name and ID checks, simplified FameELM constructor --- tests/test_OBD.py | 47 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index bd6fedd2..9a844b37 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -18,10 +18,8 @@ class FakeELM: Fake ELM327 driver class for intercepting the commands from the API """ - def __init__(self, portname, baudrate, protocol): + def __init__(self, portname, UNUSED_baudrate=None, UNUSED_protocol=None): self._portname = portname - self._baudrate = baudrate - self._protocol = protocol self._status = OBDStatus.CAR_CONNECTED self._last_command = None @@ -79,7 +77,7 @@ def test_is_connected(): assert not o.is_connected() # our fake ELM class always returns success for connections - o.port = FakeELM("/dev/null", 34800, None) + o.port = FakeELM("/dev/null") assert o.is_connected() @@ -91,9 +89,12 @@ def test_status(): o = obd.OBD("/dev/null") assert o.status() == OBDStatus.NOT_CONNECTED + o.port = None + assert o.status() == OBDStatus.NOT_CONNECTED + # we can manually set our fake ELM class to test # the other values - o.port = FakeELM("/dev/null", 34800, None) + o.port = FakeELM("/dev/null") o.port._status = OBDStatus.ELM_CONNECTED assert o.status() == OBDStatus.ELM_CONNECTED @@ -121,16 +122,44 @@ def test_port_name(): same values as the underlying ELM327 class. """ o = obd.OBD("/dev/null") - o.port = FakeELM("/dev/null", 34800, None) + o.port = FakeELM("/dev/null") assert o.port_name() == o.port._portname - o.port = FakeELM("A different port name", 34800, None) + o.port = FakeELM("A different port name") assert o.port_name() == o.port._portname +def test_protocol_name(): + o = obd.OBD("/dev/null") + + o.port = None + assert o.protocol_name() == "" + + o.port = FakeELM("/dev/null") + assert o.protocol_name() == o.port.protocol_name() + + +def test_protocol_id(): + o = obd.OBD("/dev/null") + + o.port = None + assert o.protocol_id() == "" + + o.port = FakeELM("/dev/null") + assert o.protocol_id() == o.port.protocol_id() + + + + + + +""" + The following tests are for the query() function +""" + def test_force(): o = obd.OBD("/dev/null", fast=False) # disable the trailing response count byte - o.port = FakeELM("/dev/null", 34800, None) + o.port = FakeELM("/dev/null") # a command marked as unsupported obd.commands.RPM.supported = False @@ -155,7 +184,7 @@ def test_force(): def test_fast(): o = obd.OBD("/dev/null", fast=False) - o.port = FakeELM("/dev/null", 34800, None) + o.port = FakeELM("/dev/null") assert command.fast From a2e753c7f96645d9580d631a5da02a20e67506f8 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 25 Jan 2016 21:59:02 -0500 Subject: [PATCH 310/569] moved old end-to-end test code to its own file --- tests/test_OBD.py | 92 ---------------------------------------- tests/test_end_to_end.py | 86 +++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 92 deletions(-) create mode 100644 tests/test_end_to_end.py diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 9a844b37..466d286a 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -190,95 +190,3 @@ def test_fast(): assert command.fast o.query(command, force=True) # force since this command isn't in the tables # assert o.port._test_last_command(command.command) - - - - - - - -""" -# TODO: rewrite for new protocol architecture -def test_query(): - # we don't need an actual serial connection - o = obd.OBD("/dev/null") - # forge our own command, to control the output - cmd = OBDCommand("TEST", "Test command", "0123", 2, noop, False) - - # forge IO from the car by overwriting the read/write functions - - # buffers - toCar = [""] # needs to be inside mutable object to allow assignment in closure - fromCar = "" - - def write(cmd): - toCar[0] = cmd - - o.is_connected = lambda *args: True - o.port.is_connected = lambda *args: True - o.port._ELM327__status = OBDStatus.CAR_CONNECTED - o.port._ELM327__protocol = SAE_J1850_PWM([]) - o.port._ELM327__primary_ecu = 0x10 - o.port._ELM327__write = write - o.port._ELM327__read = lambda *args: fromCar - - # make sure unsupported commands don't write ------------------------------ - fromCar = ["48 6B 10 41 23 AB CD 10"] - r = o.query(cmd) - assert toCar[0] == "" - assert r.is_null() - - # a correct command transaction ------------------------------------------- - fromCar = ["48 6B 10 41 23 AB CD 10"] # preset the response - r = o.query(cmd, force=True) # run - assert toCar[0] == "0123" # verify that the command was sent correctly - assert not r.is_null() - assert r.value == "ABCD" # verify that the response was parsed correctly - - # response of greater length ---------------------------------------------- - fromCar = ["48 6B 10 41 23 AB CD EF 10"] - r = o.query(cmd, force=True) - assert toCar[0] == "0123" - assert r.value == "ABCD" - - # response of lesser length ----------------------------------------------- - fromCar = ["48 6B 10 41 23 AB 10"] - r = o.query(cmd, force=True) - assert toCar[0] == "0123" - assert r.value == "AB00" - - # NO DATA response -------------------------------------------------------- - fromCar = ["NO DATA"] - r = o.query(cmd, force=True) - assert r.is_null() - - # malformed response ------------------------------------------------------ - fromCar = ["totaly not hex!@#$"] - r = o.query(cmd, force=True) - assert r.is_null() - - # no response ------------------------------------------------------------- - fromCar = [""] - r = o.query(cmd, force=True) - assert r.is_null() - - # reject responses from other ECUs --------------------------------------- - fromCar = ["48 6B 12 41 23 AB CD 10"] - r = o.query(cmd, force=True) - assert toCar[0] == "0123" - assert r.is_null() - - # filter for primary ECU -------------------------------------------------- - fromCar = ["48 6B 12 41 23 AB CD 10", "48 6B 10 41 23 AB CD 10"] - r = o.query(cmd, force=True) - assert toCar[0] == "0123" - assert r.value == "ABCD" - - ''' - # ignore multiline responses ---------------------------------------------- - fromCar = ["48 6B 10 41 23 AB CD 10", "48 6B 10 41 23 AB CD 10"] - r = o.query(cmd, force=True) - assert toCar[0] == "0123" - assert r.is_null() - ''' -""" diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py new file mode 100644 index 00000000..afc537c2 --- /dev/null +++ b/tests/test_end_to_end.py @@ -0,0 +1,86 @@ + +""" +# TODO: rewrite for new protocol architecture +def test_query(): + # we don't need an actual serial connection + o = obd.OBD("/dev/null") + # forge our own command, to control the output + cmd = OBDCommand("TEST", "Test command", "0123", 2, noop, False) + + # forge IO from the car by overwriting the read/write functions + + # buffers + toCar = [""] # needs to be inside mutable object to allow assignment in closure + fromCar = "" + + def write(cmd): + toCar[0] = cmd + + o.is_connected = lambda *args: True + o.port.is_connected = lambda *args: True + o.port._ELM327__status = OBDStatus.CAR_CONNECTED + o.port._ELM327__protocol = SAE_J1850_PWM([]) + o.port._ELM327__primary_ecu = 0x10 + o.port._ELM327__write = write + o.port._ELM327__read = lambda *args: fromCar + + # make sure unsupported commands don't write ------------------------------ + fromCar = ["48 6B 10 41 23 AB CD 10"] + r = o.query(cmd) + assert toCar[0] == "" + assert r.is_null() + + # a correct command transaction ------------------------------------------- + fromCar = ["48 6B 10 41 23 AB CD 10"] # preset the response + r = o.query(cmd, force=True) # run + assert toCar[0] == "0123" # verify that the command was sent correctly + assert not r.is_null() + assert r.value == "ABCD" # verify that the response was parsed correctly + + # response of greater length ---------------------------------------------- + fromCar = ["48 6B 10 41 23 AB CD EF 10"] + r = o.query(cmd, force=True) + assert toCar[0] == "0123" + assert r.value == "ABCD" + + # response of lesser length ----------------------------------------------- + fromCar = ["48 6B 10 41 23 AB 10"] + r = o.query(cmd, force=True) + assert toCar[0] == "0123" + assert r.value == "AB00" + + # NO DATA response -------------------------------------------------------- + fromCar = ["NO DATA"] + r = o.query(cmd, force=True) + assert r.is_null() + + # malformed response ------------------------------------------------------ + fromCar = ["totaly not hex!@#$"] + r = o.query(cmd, force=True) + assert r.is_null() + + # no response ------------------------------------------------------------- + fromCar = [""] + r = o.query(cmd, force=True) + assert r.is_null() + + # reject responses from other ECUs --------------------------------------- + fromCar = ["48 6B 12 41 23 AB CD 10"] + r = o.query(cmd, force=True) + assert toCar[0] == "0123" + assert r.is_null() + + # filter for primary ECU -------------------------------------------------- + fromCar = ["48 6B 12 41 23 AB CD 10", "48 6B 10 41 23 AB CD 10"] + r = o.query(cmd, force=True) + assert toCar[0] == "0123" + assert r.value == "ABCD" + + ''' + # ignore multiline responses ---------------------------------------------- + fromCar = ["48 6B 10 41 23 AB CD 10", "48 6B 10 41 23 AB CD 10"] + r = o.query(cmd, force=True) + assert toCar[0] == "0123" + assert r.is_null() + ''' +""" From 063ec83b1e7e0911414a0fcc0f44bc1adbda74f7 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 27 Jan 2016 16:18:24 -0500 Subject: [PATCH 311/569] using actual bytearrays since b'' are strings in python2 --- obd/decoders.py | 2 -- obd/protocols/protocol.py | 4 ++-- obd/protocols/protocol_can.py | 2 +- obd/protocols/protocol_legacy.py | 2 +- tests/test_OBD.py | 2 +- tests/test_decoders.py | 4 ++-- tests/test_protocol_can.py | 2 +- tests/test_protocol_legacy.py | 2 +- 8 files changed, 9 insertions(+), 11 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 22047ff8..77e32412 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -396,8 +396,6 @@ def dtc(messages): for message in messages: d += message.data - print(bytes_to_hex(d)) - # look at data in pairs of bytes # looping through ENDING indices to avoid odd (invalid) code lengths for n in range(1, len(d), 2): diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 47d4036f..e70d0a94 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -57,7 +57,7 @@ class Frame(object): """ represents a single parsed line of OBD output """ def __init__(self, raw): self.raw = raw - self.data = b'' + self.data = bytearray() self.priority = None self.addr_mode = None self.rx_id = None @@ -72,7 +72,7 @@ class Message(object): def __init__(self, frames): self.frames = frames self.ecu = ECU.UNKNOWN - self.data = b'' + self.data = bytearray() @property def tx_id(self): diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 67859d5a..de2b1319 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -64,7 +64,7 @@ def parse_frame(self, frame): if self.id_bits == 11: raw = "00000" + raw - raw_bytes = unhexlify(raw) + raw_bytes = bytearray(unhexlify(raw)) # check for valid size diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 4dc989ba..82599303 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -47,7 +47,7 @@ def parse_frame(self, frame): raw = frame.raw - raw_bytes = unhexlify(raw) + raw_bytes = bytearray(unhexlify(raw)) if len(raw_bytes) < 6: debug("Dropped frame for being too short") diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 466d286a..783f6775 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -48,7 +48,7 @@ def send_and_parse(self, cmd): # all commands succeed message = Message([]) - message.data = b'response data' + message.data = bytearray(b'response data') message.ecu = ECU.ENGINE # picked engine so that simple commands like RPM will work return [ message ] diff --git a/tests/test_decoders.py b/tests/test_decoders.py index cdb6ec73..edf0d2da 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -11,7 +11,7 @@ def m(hex_data, frames=[]): # most decoders don't look at the underlying frame objects message = Message(frames) - message.data = list(unhexlify(hex_data)) # TODO: use raw byte arrays + message.data = bytearray(unhexlify(hex_data)) # TODO: use raw byte arrays return [message] @@ -25,7 +25,7 @@ def float_equals(d1, d2): def test_noop(): - assert d.noop(m("00010203")) == ([0, 1, 2, 3], Unit.NONE) + assert d.noop(m("00010203")) == (bytearray([0, 1, 2, 3]), Unit.NONE) def test_drop(): assert d.drop(m("deadbeef")) == (None, Unit.NONE) diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index 55a5f5dc..2a8ec366 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -23,7 +23,7 @@ def check_message(m, num_frames, tx_id, data): """ generic test for correct message values """ assert len(m.frames) == num_frames assert m.tx_id == tx_id - assert m.data == bytes(data) + assert m.data == bytearray(data) diff --git a/tests/test_protocol_legacy.py b/tests/test_protocol_legacy.py index 7bdf8c25..374427cf 100644 --- a/tests/test_protocol_legacy.py +++ b/tests/test_protocol_legacy.py @@ -20,7 +20,7 @@ def check_message(m, n_frames, tx_id, data): """ generic test for correct message values """ assert len(m.frames) == n_frames assert m.tx_id == tx_id - assert m.data == bytes(data) + assert m.data == bytearray(data) def test_single_frame(): From e938a22a4d961603908681f96bd53665fd898092 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 27 Jan 2016 16:37:50 -0500 Subject: [PATCH 312/569] added tests for battery voltage, handle parse errors --- obd/decoders.py | 8 ++++++-- tests/test_decoders.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 77e32412..13c153d0 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -240,8 +240,12 @@ def elm_voltage(messages): # doesn't register as a normal OBD response, # so access the raw frame data v = messages[0].frames[0].raw - v = float(v) - return (v, Unit.VOLT) + + try: + return (float(v), Unit.VOLT) + except ValueError: + debug("Failed to parse ELM voltage", True) + return (None, Unit.NONE) ''' diff --git a/tests/test_decoders.py b/tests/test_decoders.py index edf0d2da..26d709d0 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -2,7 +2,7 @@ from binascii import unhexlify from obd.OBDResponse import Unit -from obd.protocols.protocol import Message +from obd.protocols.protocol import Frame, Message import obd.decoders as d @@ -11,7 +11,7 @@ def m(hex_data, frames=[]): # most decoders don't look at the underlying frame objects message = Message(frames) - message.data = bytearray(unhexlify(hex_data)) # TODO: use raw byte arrays + message.data = bytearray(unhexlify(hex_data)) return [message] @@ -155,6 +155,12 @@ def test_air_status(): assert d.air_status(m("08")) == ("Pump commanded on for diagnostics", Unit.NONE) assert d.air_status(m("03")) == (None, Unit.NONE) +def test_elm_voltage(): + # these aren't parsed as standard hex messages, so manufacture our own + assert d.elm_voltage([ Message([ Frame("12.875") ]) ]) == (12.875, Unit.VOLT) + assert d.elm_voltage([ Message([ Frame("12") ]) ]) == (12, Unit.VOLT) + assert d.elm_voltage([ Message([ Frame("12ABCD") ]) ]) == (None, Unit.NONE) + def test_dtc(): assert d.dtc(m("0104")) == ([ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), From c39ba5bc301b9764c70d529a14cc52f9f68f51ba Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 11 Feb 2016 10:12:25 -0500 Subject: [PATCH 313/569] updated links with my new github username --- README.md | 2 +- mkdocs.yml | 2 +- obd/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e8459fd6..3bc11a5a 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Available at [python-obd.readthedocs.org](http://python-obd.readthedocs.org/en/l Commands -------- -Here are a handful of the supported commands (sensors). For a full list, see [the wiki](https://github.com/brendanwhitfield/python-OBD/wiki/Command-Tables) +Here are a handful of the supported commands (sensors). For a full list, see [the docs](http://python-obd.readthedocs.org/en/latest/Commands/#mode-01) *note: support for these commands will vary from car to car* diff --git a/mkdocs.yml b/mkdocs.yml index 2a6dc499..d0800f49 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: python-OBD -repo_url: https://github.com/brendanwhitfield/python-OBD +repo_url: https://github.com/brendan-w/python-OBD repo_name: GitHub pages: - 'Getting Started': 'index.md' diff --git a/obd/__init__.py b/obd/__init__.py index 74153872..2c5c9e6a 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -3,7 +3,7 @@ A serial module for accessing data from a vehicles OBD-II port For more documentation, visit: - https://github.com/brendanwhitfield/python-OBD/wiki + https://github.com/brendan-w/python-OBD/wiki """ ######################################################################## diff --git a/setup.py b/setup.py index 02f47254..5e8ce5a8 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ keywords="obd obd-II obd-ii obd2 car serial vehicle diagnostic", author="Brendan Whitfield", author_email="brendanw@windworksdesign.com", - url="http://github.com/brendanwhitfield/python-OBD", + url="http://github.com/brendan-w/python-OBD", license="GNU GPLv2", packages=find_packages(), include_package_data=True, From e85e48f59ac88740d288bf8101f76872a83afb4a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 22 Mar 2016 19:20:22 -0400 Subject: [PATCH 314/569] don't need time --- obd/obd.py | 1 - 1 file changed, 1 deletion(-) diff --git a/obd/obd.py b/obd/obd.py index 567747b2..c5b7ad3d 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -29,7 +29,6 @@ # # ######################################################################## -import time from .__version__ import __version__ from .elm327 import ELM327 From 3b0a211a688d573f069079c039f1e9ebd4742908 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 22 Mar 2016 19:23:51 -0400 Subject: [PATCH 315/569] added manual protocol selection capabilities --- obd/elm327.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 53df9911..ab48b117 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -54,7 +54,9 @@ class ELM327: """ _SUPPORTED_PROTOCOLS = { - #"0" : None, # automatic mode + #"0" : None, # Automatic Mode. This isn't an actual protocol. If the + # ELM reports this, then we don't have enough + # information. see auto_protocol() "1" : SAE_J1850_PWM, "2" : SAE_J1850_VPW, "3" : ISO_9141_2, @@ -141,15 +143,39 @@ def __init__(self, portname, baudrate, protocol): self.__status = OBDStatus.ELM_CONNECTED # try to communicate with the car, and load the correct protocol parser - if self.load_protocol(): + if self.load_protocol(protocol): self.__status = OBDStatus.CAR_CONNECTED debug("Connection successful") else: debug("Connected to the adapter, but failed to connect to the vehicle", True) + def load_protocol(self, protocol): + if protocol is not None: + # an explicit protocol was specified + if protocol not in self._SUPPORTED_PROTOCOLS: + debug("%s is not a valid protocol. Please use \"1\" through \"A\"", True) + return False + return self.manual_protocol(protocol) + else: + # auto detect the protocol + return self.auto_protocol() + + + def manual_protocol(self, protocol): + + r = self.__send("ATTP%s" % p) + r0100 = self.__send("0100") + + if not self.__has_message(r0100, "UNABLE TO CONNECT"): + # success, found the protocol + self.__protocol = self._SUPPORTED_PROTOCOLS[p](r0100) + return True + + return False + - def load_protocol(self): + def auto_protocol(self): """ Attempts communication with the car. From 23dac92e2184d615b73e7a75998cfe1a8ef04c39 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 22 Mar 2016 20:13:11 -0400 Subject: [PATCH 316/569] fixed var name for manual protocols --- obd/elm327.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index ab48b117..062ac097 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -164,12 +164,12 @@ def load_protocol(self, protocol): def manual_protocol(self, protocol): - r = self.__send("ATTP%s" % p) + r = self.__send("ATTP%s" % protocol) r0100 = self.__send("0100") if not self.__has_message(r0100, "UNABLE TO CONNECT"): # success, found the protocol - self.__protocol = self._SUPPORTED_PROTOCOLS[p](r0100) + self.__protocol = self._SUPPORTED_PROTOCOLS[protocol](r0100) return True return False From 553d74882c75d5f58631b908ccdf1eeff602fce9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 22 Mar 2016 20:45:08 -0400 Subject: [PATCH 317/569] keep support status within the OBD session object noting support status on the command objects themselves is messy, and does not allow for more than one connection. Instead, the supported_commands attribute is now being used to denote support status --- obd/OBDCommand.py | 7 ++----- obd/async.py | 2 +- obd/commands.py | 25 +++++++++++++++++++------ obd/obd.py | 19 +++++++++---------- 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 5226a0b3..d860f38e 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -43,8 +43,7 @@ def __init__(self, _bytes, decoder, ecu=ECU.ALL, - fast=False, - supported=False): + fast=False): self.name = name # human readable name (also used as key in commands dict) self.desc = desc # human readable description self.command = command # command string @@ -52,7 +51,6 @@ def __init__(self, self.decode = decoder # decoding function self.ecu = ecu # ECU ID from which this command expects messages from self.fast = fast # can an extra digit be added to the end of the command? (to make the ELM return early) - self.supported = supported # bool for support def clone(self): return OBDCommand(self.name, @@ -61,8 +59,7 @@ def clone(self): self.bytes, self.decode, self.ecu, - self.fast, - self.supported) + self.fast) @property def mode_int(self): diff --git a/obd/async.py b/obd/async.py index 28be7729..264600da 100644 --- a/obd/async.py +++ b/obd/async.py @@ -133,7 +133,7 @@ def watch(self, c, callback=None, force=False): debug("Can't watch() while running, please use stop()", True) else: - if not (self.supports(c) or force): + if not force and not self.supports(c): debug("'%s' is not supported" % str(c), True) return diff --git a/obd/commands.py b/obd/commands.py index 9bef1d0c..984513b8 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -48,7 +48,7 @@ __mode1__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "0100", 4, pid, ECU.ENGINE, True, True), # the first PID getter is assumed to be supported + OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "0100", 4, pid, ECU.ENGINE, True), OBDCommand("STATUS" , "Status since DTCs cleared" , "0101", 4, status, ECU.ENGINE, True), OBDCommand("FREEZE_DTC" , "Freeze DTC" , "0102", 2, drop, ECU.ENGINE, True), OBDCommand("FUEL_STATUS" , "Fuel System Status" , "0103", 2, fuel_status, ECU.ENGINE, True), @@ -163,23 +163,22 @@ __mode3__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("GET_DTC" , "Get DTCs" , "03", 0, dtc, ECU.ALL, False, True), + OBDCommand("GET_DTC" , "Get DTCs" , "03", 0, dtc, ECU.ALL, False), ] __mode4__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", 0, drop, ECU.ALL, False, True), + OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", 0, drop, ECU.ALL, False), ] __mode7__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", 0, dtc, ECU.ALL, False, True), + OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", 0, dtc, ECU.ALL, False), ] __misc__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("VOLTAGE" , "Voltage detected by OBD-II adapter" , "ATRV", 0, elm_voltage, ECU.UNKNOWN, False, True), -] + OBDCommand("VOLTAGE" , "Voltage detected by OBD-II adapter" , "ATRV", 0, elm_voltage, ECU.UNKNOWN, False),] @@ -241,6 +240,20 @@ def __contains__(self, s): return self.has_name(s) + def base_commands(self): + """ + returns the list of commands that should always be + supported by the ELM327 + """ + return [ + self.PIDS_A, + self.GET_DTC, + self.CLEAR_DTC, + self.GET_FREEZE_DTC, + self.VOLTAGE, + ] + + def pid_getters(self): """ returns a list of PID GET commands """ getters = [] diff --git a/obd/obd.py b/obd/obd.py index c5b7ad3d..b2b27974 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -47,7 +47,7 @@ class OBD(object): def __init__(self, portstr=None, baudrate=38400, protocol=None, fast=True): self.port = None - self.supported_commands = [] + self.supported_commands = set(commands.base_commands()) self.fast = fast self.__last_command = "" # used for running the previous command with a CR @@ -122,13 +122,12 @@ def __load_commands(self): pid = get.pid_int + i + 1 if commands.has_pid(mode, pid): - c = commands[mode][pid] - c.supported = True - - # don't add PID getters to the command list - if c not in pid_getters: - self.supported_commands.append(c) + self.supported_commands.add(commands[mode][pid]) + # set support for mode 2 commands + if mode == 1 and commands.has_pid(2, pid): + self.supported_commands.add(commands[2][pid]) + debug("finished querying with %d commands supported" % len(self.supported_commands)) @@ -215,9 +214,9 @@ def print_commands(self): def supports(self, cmd): """ Returns a boolean for whether the given command - is supported by the car AND this library + is supported by the car """ - return commands.has_command(cmd) and cmd.supported + return cmd in self.supported_commands def query(self, cmd, force=False): @@ -230,7 +229,7 @@ def query(self, cmd, force=False): debug("Query failed, no connection available", True) return OBDResponse() - if not self.supports(cmd) and not force: + if not force and not self.supports(cmd): debug("'%s' is not supported" % str(cmd), True) return OBDResponse() From d5a62f859347c2f09da42d48dfd855db4eb2afd9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 22 Mar 2016 20:50:33 -0400 Subject: [PATCH 318/569] fixed tests for new command support system --- tests/test_OBD.py | 3 +-- tests/test_OBDCommand.py | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 783f6775..1c26ed4e 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -65,7 +65,6 @@ def _test_last_command(self, expected): 0, \ noop, \ ECU.ALL, \ - True, \ True) @@ -109,7 +108,7 @@ def test_supports(): # since we haven't actually connected, # no commands should be marked as supported assert not o.supports(obd.commands.RPM) - obd.commands.RPM.supported = True + o.supported_commands.add(obd.commands.RPM) assert o.supports(obd.commands.RPM) # commands that aren't in python-OBD's tables are unsupported by default diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index 4e5498d8..b0bb4aa7 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -18,16 +18,14 @@ def test_constructor(): assert cmd.decode == noop assert cmd.ecu == ECU.ENGINE assert cmd.fast == False - assert cmd.supported == False assert cmd.mode_int == 1 assert cmd.pid_int == 35 # a case where "fast", and "supported" were set explicitly - # name description cmd bytes decoder ECU fast supported - cmd = OBDCommand("Test 2", "example OBD command", "0123", 2, noop, ECU.ENGINE, True, True) + # name description cmd bytes decoder ECU fast + cmd = OBDCommand("Test 2", "example OBD command", "0123", 2, noop, ECU.ENGINE, True) assert cmd.fast == True - assert cmd.supported == True @@ -43,7 +41,6 @@ def test_clone(): assert cmd.decode == other.decode assert cmd.ecu == other.ecu assert cmd.fast == cmd.fast - assert cmd.supported == cmd.supported From 284aada689f074b4099705e65a2e769e0f5a87e7 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 22 Mar 2016 21:03:54 -0400 Subject: [PATCH 319/569] updated docs for command support status --- docs/Custom Commands.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/Custom Commands.md b/docs/Custom Commands.md index 51cf80fc..fda7c403 100644 --- a/docs/Custom Commands.md +++ b/docs/Custom Commands.md @@ -10,7 +10,6 @@ If the command you need is not in python-OBDs tables, you can create a new `OBDC | decoder | callable | Function used for decoding the hex response | | ecu (optional) | ECU | ID of the ECU this command should listen to (`ECU.ALL` by default) | | fast (optional) | bool | Allows python-OBD to alter this command for efficieny (`False` by default) | -| supported (optional) | bool | Flag to prevent the sending of unsupported commands (`False` by default) | Example @@ -32,14 +31,30 @@ c = OBDCommand("RPM", \ # name 2, \ # number of return bytes to expect rpm, \ # decoding function ECU.ENGINE, \ # (optional) ECU filter - True, \ # (optional) allow a "01" to be added for speed - True), # (optional) supported by deafult + True) # (optional) allow a "01" to be added for speed ``` -Here are some details on the less intuitive fields: +By default, custom commands will be treated as "unsupported by the vehicle". There are two ways to handle this: + +```python +# use the `force` parameter when querying +o = obd.OBD() +o.query(c, force=True) +``` + +or + +```python +# add your command to the set of supported commands +o = obd.OBD() +o.supported_commands.add(c) +o.query(c) +```
+Here are some details on the less intuitive fields of an OBDCommand: + --- ### OBDCommand.decoder From 25c24a45ab85d9090c61bfcc3ec7c727455d533d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 23 Mar 2016 14:46:09 -0400 Subject: [PATCH 320/569] added ELM version command --- obd/commands.py | 7 +++++-- obd/decoders.py | 9 +++++++++ tests/test_decoders.py | 6 ++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 984513b8..e166d4a5 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -178,7 +178,9 @@ __misc__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("VOLTAGE" , "Voltage detected by OBD-II adapter" , "ATRV", 0, elm_voltage, ECU.UNKNOWN, False),] + OBDCommand("ELM_VERSION" , "ELM327 version string" , "ATI", 0, raw_string, ECU.UNKNOWN, False), + OBDCommand("ELM_VOLTAGE" , "Voltage detected by OBD-II adapter" , "ATRV", 0, elm_voltage, ECU.UNKNOWN, False), +] @@ -250,7 +252,8 @@ def base_commands(self): self.GET_DTC, self.CLEAR_DTC, self.GET_FREEZE_DTC, - self.VOLTAGE, + self.ELM_VERSION, + self.ELM_VOLTAGE, ] diff --git a/obd/decoders.py b/obd/decoders.py index 13c153d0..6b50879a 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -61,6 +61,15 @@ def pid(messages): v = bytes_to_bits(d) return (v, Unit.NONE) +# returns the raw strings from the ELM +def raw_string(messages): + strings = [] + + for m in messages: + strings += [f.raw for f in m.frames] + + return ("\n".join(strings), Unit.NONE) + ''' Sensor decoders Return Value object with value and units diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 26d709d0..cd6b783f 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -30,6 +30,12 @@ def test_noop(): def test_drop(): assert d.drop(m("deadbeef")) == (None, Unit.NONE) +def test_raw_string(): + assert d.raw_string([ Message([]) ]) == ("", Unit.NONE) + assert d.raw_string([ Message([ Frame("NO DATA") ]) ]) == ("NO DATA", Unit.NONE) + assert d.raw_string([ Message([ Frame("A"), Frame("B") ]) ]) == ("A\nB", Unit.NONE) + assert d.raw_string([ Message([ Frame("A") ]), Message([ Frame("B") ]) ]) == ("A\nB", Unit.NONE) + def test_pid(): assert d.pid(m("00000000")) == ("00000000000000000000000000000000", Unit.NONE) assert d.pid(m("F00AA00F")) == ("11110000000010101010000000001111", Unit.NONE) From 0bae63a8554d34f8152114428a57cdee7224c81d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 23 Mar 2016 14:52:38 -0400 Subject: [PATCH 321/569] added ELM commands to the docs --- docs/Commands.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/Commands.md b/docs/Commands.md index 4d966560..ab670aee 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -61,6 +61,15 @@ obd.commands.has_pid(1, 12) # True
+# OBD-II adapter (ELM327 commands) + +|PID | Name | Description | +|-----|-------------|-----------------------------------------| +| N/A | ELM_VERSION | OBD-II adapter version string | +| N/A | ELM_VOLTAGE | Voltage detected by OBD-II adapter | + +
+ # Mode 01 |PID | Name | Description | From 0c28ed6fb6e8a1fe0beb401c85a714a6ac1a56ad Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 23 Mar 2016 15:19:44 -0400 Subject: [PATCH 322/569] moved stringifying to the Message class --- docs/Custom Commands.md | 3 +-- obd/decoders.py | 7 +------ obd/protocols/protocol.py | 4 ++++ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/Custom Commands.md b/docs/Custom Commands.md index fda7c403..4d63203b 100644 --- a/docs/Custom Commands.md +++ b/docs/Custom Commands.md @@ -7,7 +7,7 @@ If the command you need is not in python-OBDs tables, you can create a new `OBDC | desc | string | (human readability only) | | command | string | OBD command in hex (typically mode + PID | | bytes | int | Number of bytes expected in response | -| decoder | callable | Function used for decoding the hex response | +| decoder | callable | Function used for decoding messages from the OBD adapter | | ecu (optional) | ECU | ID of the ECU this command should listen to (`ECU.ALL` by default) | | fast (optional) | bool | Allows python-OBD to alter this command for efficieny (`False` by default) | @@ -76,7 +76,6 @@ def (messages): _hex = messages[0].hex() ... return (, ) - ``` --- diff --git a/obd/decoders.py b/obd/decoders.py index 6b50879a..f0449adb 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -63,12 +63,7 @@ def pid(messages): # returns the raw strings from the ELM def raw_string(messages): - strings = [] - - for m in messages: - strings += [f.raw for f in m.frames] - - return ("\n".join(strings), Unit.NONE) + return ("\n".join([str(m) for m in messages]), Unit.NONE) ''' Sensor decoders diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index e70d0a94..a3538bd9 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -88,6 +88,10 @@ def parsed(self): """ boolean for whether this message was successfully parsed """ return bool(self.data) + def __str__(self): + """ returns the original raw input string from the adapter """ + return "\n".join([f.raw for f in self.frames]) + def __eq__(self, other): if isinstance(other, Message): for attr in ["frames", "ecu", "data"]: From 2b1a2bfe0a506104edd3bd3ec9b520d85bd738f5 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 23 Mar 2016 15:21:54 -0400 Subject: [PATCH 323/569] using .raw() instead of str() --- obd/decoders.py | 2 +- obd/protocols/protocol.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index f0449adb..f6767b1a 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -63,7 +63,7 @@ def pid(messages): # returns the raw strings from the ELM def raw_string(messages): - return ("\n".join([str(m) for m in messages]), Unit.NONE) + return ("\n".join([m.raw() for m in messages]), Unit.NONE) ''' Sensor decoders diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index a3538bd9..efe83a9a 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -84,14 +84,14 @@ def tx_id(self): def hex(self): return hexlify(self.data) + def raw(self): + """ returns the original raw input string from the adapter """ + return "\n".join([f.raw for f in self.frames]) + def parsed(self): """ boolean for whether this message was successfully parsed """ return bool(self.data) - def __str__(self): - """ returns the original raw input string from the adapter """ - return "\n".join([f.raw for f in self.frames]) - def __eq__(self, other): if isinstance(other, Message): for attr in ["frames", "ecu", "data"]: From cf9e577656e9c900d277304e86c7408f63818004 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 23 Mar 2016 15:24:27 -0400 Subject: [PATCH 324/569] wrote a note in the docs about Message.raw() --- docs/Custom Commands.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/Custom Commands.md b/docs/Custom Commands.md index 4d63203b..150cb412 100644 --- a/docs/Custom Commands.md +++ b/docs/Custom Commands.md @@ -78,6 +78,8 @@ def (messages): return (, ) ``` +*You can also access the original string sent by the adapter using the `Message.raw()` function.* + --- ### OBDCommand.ecu From f931d8fd29599b6812f57223e26b654fac3bac83 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 29 Mar 2016 16:36:47 -0400 Subject: [PATCH 325/569] added deprecation warning to OBDCommand.supported --- obd/OBDCommand.py | 6 ++++++ obd/obd.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index d860f38e..65a365e5 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -75,6 +75,12 @@ def pid_int(self): else: return 0 + # TODO: remove later + @property + def supported(self): + debug("OBDCommand.supported is deprecated. Use OBD.supports() instead", True) + return False + def __call__(self, messages): # filter for applicable messages (from the right ECU(s)) diff --git a/obd/obd.py b/obd/obd.py index b2b27974..11c81e2e 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -111,7 +111,7 @@ def __load_commands(self): if response.is_null(): continue - + supported = response.value # string of binary 01010101010101 # loop through PIDs binary @@ -127,7 +127,7 @@ def __load_commands(self): # set support for mode 2 commands if mode == 1 and commands.has_pid(2, pid): self.supported_commands.add(commands[2][pid]) - + debug("finished querying with %d commands supported" % len(self.supported_commands)) From a2e5846c5683cd157a0f757874ab9cf1b76ad04b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 2 Apr 2016 20:35:13 -0400 Subject: [PATCH 326/569] fixed removed OBDCommand.supported assignment --- tests/test_OBD.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 1c26ed4e..35b22b81 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -132,7 +132,7 @@ def test_protocol_name(): o = obd.OBD("/dev/null") o.port = None - assert o.protocol_name() == "" + assert o.protocol_name() == "" o.port = FakeELM("/dev/null") assert o.protocol_name() == o.port.protocol_name() @@ -142,7 +142,7 @@ def test_protocol_id(): o = obd.OBD("/dev/null") o.port = None - assert o.protocol_id() == "" + assert o.protocol_id() == "" o.port = FakeELM("/dev/null") assert o.protocol_id() == o.port.protocol_id() @@ -160,9 +160,6 @@ def test_force(): o = obd.OBD("/dev/null", fast=False) # disable the trailing response count byte o.port = FakeELM("/dev/null") - # a command marked as unsupported - obd.commands.RPM.supported = False - r = o.query(obd.commands.RPM) assert r.is_null() assert o.port._test_last_command(None) @@ -184,7 +181,6 @@ def test_force(): def test_fast(): o = obd.OBD("/dev/null", fast=False) o.port = FakeELM("/dev/null") - assert command.fast o.query(command, force=True) # force since this command isn't in the tables From 14c0876979f2646a439aca64599b3fb6256f3964 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 2 Apr 2016 20:40:18 -0400 Subject: [PATCH 327/569] fixed testing doc link --- tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index e264e0c7..a43393f3 100644 --- a/tests/README.md +++ b/tests/README.md @@ -7,4 +7,4 @@ To run all tests, run the following command: $ py.test tests/ -For more information on pytest with virtualenvs, [read more here](http://pytest.org/latest/goodpractises.html) \ No newline at end of file +For more information on pytest with virtualenvs, [read more here](https://pytest.org/dev/goodpractises.html) \ No newline at end of file From f5615809d3755cfff9866f7ed1eb476a0dd9eb84 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Apr 2016 01:30:27 -0400 Subject: [PATCH 328/569] added basic obdsim test --- tests/README.md | 4 ++-- tests/conftest.py | 6 ++++++ tests/test_obdsim.py | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_obdsim.py diff --git a/tests/README.md b/tests/README.md index a43393f3..e21d0695 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,10 +1,10 @@ Testing ======= -To test python-OBD, you will need to `pip install pytest` and install the module (preferably in a virtualenv) by running `python setup.py install` +To test python-OBD, you will need to `pip install pytest` and install the module (preferably in a virtualenv) by running `python setup.py install`. The end-to-end tests will also require [obdsim](http://icculus.org/obdgpslogger/obdsim.html) to be running in the background. When starting obdsim, note the "SimPort name" that it creates, and pass it as an argument to py.test. To run all tests, run the following command: - $ py.test tests/ + $ py.test --obdsim=/dev/pts/ For more information on pytest with virtualenvs, [read more here](https://pytest.org/dev/goodpractises.html) \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..07e2a116 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,6 @@ + +import pytest + +def pytest_addoption(parser): + parser.addoption("--obdsim", action="store", default=None, + help="pts file name for obdsim tests") diff --git a/tests/test_obdsim.py b/tests/test_obdsim.py new file mode 100644 index 00000000..4d54770f --- /dev/null +++ b/tests/test_obdsim.py @@ -0,0 +1,22 @@ + +import pytest +from obd import commands, Unit + + +@pytest.fixture(scope="module") +def obd(request): + """provides an OBD connection object for obdsim""" + import obd + port = request.config.getoption("--obdsim") + + # TODO: lookup how to fail inside of a fixture + if port is None: + print("Please run obdsim and use --obdsim=") + exit(1) + return obd.OBD(port) + + +def test_rpm(obd): + r = obd.query(commands.RPM) + assert(isinstance(r.value, float)) + assert(r.unit == Unit.RPM) From 7e85e06aa4a939a5ab2b7609ed02bb384419ac42 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Apr 2016 02:06:42 -0400 Subject: [PATCH 329/569] added async tests --- tests/test_obdsim.py | 59 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/tests/test_obdsim.py b/tests/test_obdsim.py index 4d54770f..d1442ae3 100644 --- a/tests/test_obdsim.py +++ b/tests/test_obdsim.py @@ -1,4 +1,5 @@ +import time import pytest from obd import commands, Unit @@ -13,10 +14,64 @@ def obd(request): if port is None: print("Please run obdsim and use --obdsim=") exit(1) + return obd.OBD(port) +@pytest.fixture(scope="module") +def async(request): + """provides an OBD *Async* connection object for obdsim""" + import obd + port = request.config.getoption("--obdsim") + + # TODO: lookup how to fail inside of a fixture + if port is None: + print("Please run obdsim and use --obdsim=") + exit(1) + + return obd.Async(port) + + +def good_rpm_response(r): + return isinstance(r.value, float) and \ + r.value > 0.0 and \ + r.unit == Unit.RPM + +def test_supports(obd): + assert(len(obd.supported_commands) > 0) + assert(obd.supports(commands.RPM)) + + def test_rpm(obd): r = obd.query(commands.RPM) - assert(isinstance(r.value, float)) - assert(r.unit == Unit.RPM) + assert(good_rpm_response(r)) + + +def test_async_query(async): + + rs = [] + async.watch(commands.RPM) + async.start() + + for i in range(5): + time.sleep(0.05) + rs.append(async.query(commands.RPM)) + + async.stop() + + # make sure we got data + assert(len(rs) > 0) + assert(all([ good_rpm_response(r) for r in rs ])) + + +def test_async_callback(async): + + rs = [] + async.watch(commands.RPM, callback=rs.append) + async.start() + time.sleep(0.05) + async.stop() + + # make sure we got data + assert(len(rs) > 0) + assert(all([ good_rpm_response(r) for r in rs ])) From 3b7cdf71ecb95439b448835f504e286b51eec0d3 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Apr 2016 14:39:39 -0400 Subject: [PATCH 330/569] added more async tests --- tests/README.md | 2 +- tests/conftest.py | 4 +-- tests/test_obdsim.py | 84 ++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/tests/README.md b/tests/README.md index e21d0695..2ecfa72e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -5,6 +5,6 @@ To test python-OBD, you will need to `pip install pytest` and install the module To run all tests, run the following command: - $ py.test --obdsim=/dev/pts/ + $ py.test --port=/dev/pts/ For more information on pytest with virtualenvs, [read more here](https://pytest.org/dev/goodpractises.html) \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 07e2a116..b4216842 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,5 +2,5 @@ import pytest def pytest_addoption(parser): - parser.addoption("--obdsim", action="store", default=None, - help="pts file name for obdsim tests") + parser.addoption("--port", action="store", default=None, + help="device file for doing end-to-end testing") diff --git a/tests/test_obdsim.py b/tests/test_obdsim.py index d1442ae3..248608fd 100644 --- a/tests/test_obdsim.py +++ b/tests/test_obdsim.py @@ -3,16 +3,18 @@ import pytest from obd import commands, Unit +STANDARD_WAIT_TIME = 0.1 + @pytest.fixture(scope="module") def obd(request): """provides an OBD connection object for obdsim""" import obd - port = request.config.getoption("--obdsim") + port = request.config.getoption("--port") # TODO: lookup how to fail inside of a fixture if port is None: - print("Please run obdsim and use --obdsim=") + print("Please run obdsim and use --port=") exit(1) return obd.OBD(port) @@ -22,11 +24,11 @@ def obd(request): def async(request): """provides an OBD *Async* connection object for obdsim""" import obd - port = request.config.getoption("--obdsim") + port = request.config.getoption("--port") # TODO: lookup how to fail inside of a fixture if port is None: - print("Please run obdsim and use --obdsim=") + print("Please run obdsim and use --port=") exit(1) return obd.Async(port) @@ -47,6 +49,8 @@ def test_rpm(obd): assert(good_rpm_response(r)) +# Async tests + def test_async_query(async): rs = [] @@ -54,10 +58,11 @@ def test_async_query(async): async.start() for i in range(5): - time.sleep(0.05) + time.sleep(STANDARD_WAIT_TIME) rs.append(async.query(commands.RPM)) async.stop() + async.unwatch_all() # make sure we got data assert(len(rs) > 0) @@ -69,9 +74,76 @@ def test_async_callback(async): rs = [] async.watch(commands.RPM, callback=rs.append) async.start() - time.sleep(0.05) + time.sleep(STANDARD_WAIT_TIME) async.stop() + async.unwatch_all() # make sure we got data assert(len(rs) > 0) assert(all([ good_rpm_response(r) for r in rs ])) + + +def test_async_paused(async): + + assert(not async.running) + async.watch(commands.RPM) + async.start() + assert(async.running) + + with async.paused() as was_running: + assert(not async.running) + assert(was_running) + + assert(async.running) + async.stop() + assert(not async.running) + + +def test_async_unwatch(async): + + watched_rs = [] + unwatched_rs = [] + + async.watch(commands.RPM) + async.start() + + for i in range(5): + time.sleep(STANDARD_WAIT_TIME) + watched_rs.append(async.query(commands.RPM)) + + with async.paused(): + async.unwatch(commands.RPM) + + for i in range(5): + time.sleep(STANDARD_WAIT_TIME) + unwatched_rs.append(async.query(commands.RPM)) + + async.stop() + + # the watched commands + assert(len(watched_rs) > 0) + assert(all([ good_rpm_response(r) for r in watched_rs ])) + + # the unwatched commands + assert(len(unwatched_rs) > 0) + assert(all([ r.is_null() for r in unwatched_rs ])) + + +def test_async_unwatch_callback(async): + + a_rs = [] + b_rs = [] + async.watch(commands.RPM, callback=a_rs.append) + async.watch(commands.RPM, callback=b_rs.append) + + async.start() + time.sleep(STANDARD_WAIT_TIME) + + with async.paused(): + async.unwatch(commands.RPM, callback=b_rs.append) + + time.sleep(STANDARD_WAIT_TIME) + async.unwatch_all() + + assert(all([ good_rpm_response(r) for r in a_rs + b_rs ])) + assert(len(a_rs) > len(b_rs)) From 0fa6e5e8005c3f535fa7a3270e87bea350d4ed12 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Apr 2016 14:44:52 -0400 Subject: [PATCH 331/569] removed debug enablers in tests --- tests/test_protocol_can.py | 3 --- tests/test_protocol_legacy.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index 2a8ec366..222ec2c6 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -3,9 +3,6 @@ from obd.protocols import * from obd.protocols.protocol import Message -from obd import debug -debug.console = True - CAN_11_PROTOCOLS = [ ISO_15765_4_11bit_500k, diff --git a/tests/test_protocol_legacy.py b/tests/test_protocol_legacy.py index 374427cf..37631f71 100644 --- a/tests/test_protocol_legacy.py +++ b/tests/test_protocol_legacy.py @@ -3,9 +3,6 @@ from obd.protocols import * from obd.protocols.protocol import Message -from obd import debug -debug.console = True - LEGACY_PROTOCOLS = [ SAE_J1850_PWM, From cbd160d1908118bc978db2648877c8f96390c197 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Apr 2016 14:51:58 -0400 Subject: [PATCH 332/569] forgot to stop the async loop --- tests/test_obdsim.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_obdsim.py b/tests/test_obdsim.py index 248608fd..99fe8bc0 100644 --- a/tests/test_obdsim.py +++ b/tests/test_obdsim.py @@ -143,6 +143,7 @@ def test_async_unwatch_callback(async): async.unwatch(commands.RPM, callback=b_rs.append) time.sleep(STANDARD_WAIT_TIME) + async.stop() async.unwatch_all() assert(all([ good_rpm_response(r) for r in a_rs + b_rs ])) From c3eb2f5a930e22b6ef03994a4cab9cd382b1d8cb Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Apr 2016 15:09:53 -0400 Subject: [PATCH 333/569] consistent naming convention for scan_serial() --- docs/Connections.md | 4 ++-- docs/Troubleshooting.md | 4 ++-- obd/__init__.py | 2 +- obd/obd.py | 8 ++++---- obd/utils.py | 9 +++++++-- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/Connections.md b/docs/Connections.md index 239f5c88..36965ab6 100644 --- a/docs/Connections.md +++ b/docs/Connections.md @@ -1,5 +1,5 @@ -After installing the library, simply `import obd`, and create a new OBD connection object. By default, python-OBD will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the scanSerial helper retrieve a list of connected ports. +After installing the library, simply `import obd`, and create a new OBD connection object. By default, python-OBD will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the `scan_serial` helper retrieve a list of connected ports. ```python import obd @@ -12,7 +12,7 @@ connection = obd.OBD("/dev/ttyUSB0") # create connection with USB 0 # OR -ports = obd.scanSerial() # return list of valid USB or RF ports +ports = obd.scan_serial() # return list of valid USB or RF ports print ports # ['/dev/ttyUSB0', '/dev/ttyUSB1'] connection = obd.OBD(ports[0]) # connect to the first port in the list ``` diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index cba73af2..e9819f56 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -80,12 +80,12 @@ This is likely a problem with the serial connection between the OBD-II adapter a - you are connecting to the right port in `/dev` (or that there is any port at all) - you have the correct permissions to write to the port -You can use the `scanSerial()` helper function to determine which ports are available for writing. +You can use the `scan_serial()` helper function to determine which ports are available for writing. ```python import obd -ports = obd.scanSerial() # return list of valid USB or RF ports +ports = obd.scan_serial() # return list of valid USB or RF ports print ports # ['/dev/ttyUSB0', '/dev/ttyUSB1'] ``` diff --git a/obd/__init__.py b/obd/__init__.py index 78294b4e..8ead11ca 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -43,5 +43,5 @@ from .OBDCommand import OBDCommand from .OBDResponse import OBDResponse, Unit from .protocols import ECU -from .utils import scanSerial, OBDStatus +from .utils import scan_serial, scanSerial, OBDStatus # TODO: scanSerial() deprecated from .debug import debug diff --git a/obd/obd.py b/obd/obd.py index 11c81e2e..a8b509c8 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -34,7 +34,7 @@ from .elm327 import ELM327 from .commands import commands from .OBDResponse import OBDResponse -from .utils import scanSerial, OBDStatus +from .utils import scan_serial, OBDStatus from .debug import debug @@ -63,8 +63,8 @@ def __connect(self, portstr, baudrate, protocol): """ if portstr is None: - debug("Using scanSerial to select port") - portnames = scanSerial() + debug("Using scan_serial to select port") + portnames = scan_serial() debug("Available ports: " + str(portnames)) if not portnames: @@ -258,7 +258,7 @@ def __build_command_string(self, cmd): if self.fast and cmd.fast: cmd_string += str(len(self.port.ecus())) - # if we sent this last time, just send + # if we sent this last time, just send if self.fast and (cmd_string == self.__last_command): cmd_string = "" diff --git a/obd/utils.py b/obd/utils.py index 2d86b3aa..ea3b3cd3 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -131,7 +131,7 @@ def try_port(portStr): return False -def scanSerial(): +def scan_serial(): """scan for available ports. return a list of serial names""" available = [] @@ -156,5 +156,10 @@ def scanSerial(): for port in possible_ports: if try_port(port): available.append(port) - + return available + +# TODO: deprecated, remove later +def scanSerial(): + print("scanSerial() is deprecated, use scan_serial() instead") + return scan_serial() From ad5b232ce0fc9841dacebba78f78d15c2e9473b3 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Apr 2016 15:15:23 -0400 Subject: [PATCH 334/569] fixed a few old wiki links to point to readthedocs --- README.md | 2 +- obd/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e8459fd6..37c6fdf3 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Available at [python-obd.readthedocs.org](http://python-obd.readthedocs.org/en/l Commands -------- -Here are a handful of the supported commands (sensors). For a full list, see [the wiki](https://github.com/brendanwhitfield/python-OBD/wiki/Command-Tables) +Here are a handful of the supported commands (sensors). For a full list, see [the wiki](http://python-obd.readthedocs.org/en/latest/Commands/#mode-01) *note: support for these commands will vary from car to car* diff --git a/obd/__init__.py b/obd/__init__.py index 8ead11ca..51ea205c 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -3,7 +3,7 @@ A serial module for accessing data from a vehicles OBD-II port For more documentation, visit: - https://github.com/brendanwhitfield/python-OBD/wiki + http://python-obd.readthedocs.org/en/latest/ """ ######################################################################## From 642d11086ecb1fff2495c5ad0bbe155550854b94 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Apr 2016 15:41:57 -0400 Subject: [PATCH 335/569] zero is an acceptable RPM --- tests/test_obdsim.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_obdsim.py b/tests/test_obdsim.py index 99fe8bc0..757b9a28 100644 --- a/tests/test_obdsim.py +++ b/tests/test_obdsim.py @@ -36,7 +36,7 @@ def async(request): def good_rpm_response(r): return isinstance(r.value, float) and \ - r.value > 0.0 and \ + r.value >= 0.0 and \ r.unit == Unit.RPM def test_supports(obd): From 5b5d89de7acab394384670f5e8783714a7f338d1 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Apr 2016 21:46:50 -0400 Subject: [PATCH 336/569] bump to 0.5.0 --- obd/__version__.py | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/obd/__version__.py b/obd/__version__.py index 489c7e19..700be4cb 100644 --- a/obd/__version__.py +++ b/obd/__version__.py @@ -1,2 +1,2 @@ -__version__ = '0.4.1' +__version__ = '0.5.0' diff --git a/setup.py b/setup.py index c2ca6d1e..b0119ec9 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,11 @@ #!/bin/env python -# -*- coding: utf8 -*- +# -*- coding: utf-8 -*- from setuptools import setup, find_packages setup( name="obd", - version="0.4.1", + version="0.5.0", description=("Serial module for handling live sensor data from a vehicle's OBD-II port"), classifiers=[ "Operating System :: POSIX :: Linux", From c633d048a0d0195fe536c5d365fa37a63ed2d724 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Apr 2016 21:51:20 -0400 Subject: [PATCH 337/569] increased the wait time between async queries --- tests/test_obdsim.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_obdsim.py b/tests/test_obdsim.py index 757b9a28..287ade6c 100644 --- a/tests/test_obdsim.py +++ b/tests/test_obdsim.py @@ -3,7 +3,7 @@ import pytest from obd import commands, Unit -STANDARD_WAIT_TIME = 0.1 +STANDARD_WAIT_TIME = 0.25 @pytest.fixture(scope="module") From 6631c0cdda1e47ad9455c1214456d3b64754898e Mon Sep 17 00:00:00 2001 From: msholsve Date: Thu, 14 Apr 2016 12:58:52 +0200 Subject: [PATCH 338/569] Update obd.py Cannot compare methods and strings. --- obd/obd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/obd.py b/obd/obd.py index a8b509c8..14d22580 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -75,7 +75,7 @@ def __connect(self, portstr, baudrate, protocol): debug("Attempting to use port: " + str(port)) self.port = ELM327(port, baudrate, protocol) - if self.port.status >= OBDStatus.ELM_CONNECTED: + if self.port.status() >= OBDStatus.ELM_CONNECTED: break # success! stop searching for serial else: debug("Explicit port defined") From dea30c336ca1456df15e748d5f599adfb4daca7e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 14 Apr 2016 17:14:22 -0400 Subject: [PATCH 339/569] log BEFORE calling write(), in case it blocks --- obd/elm327.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/elm327.py b/obd/elm327.py index 062ac097..0b0841f9 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -338,10 +338,10 @@ def __write(self, cmd): if self.__port: cmd += "\r\n" # terminate + debug("write: " + repr(cmd)) self.__port.flushInput() # dump everything in the input buffer self.__port.write(cmd.encode()) # turn the string into bytes and write self.__port.flush() # wait for the output buffer to finish transmitting - debug("write: " + repr(cmd)) else: debug("cannot perform __write() when unconnected", True) From 651a2125e8d88ebf1efa826cdbd02f5e4eb21431 Mon Sep 17 00:00:00 2001 From: HawtDogFlvrWtr Date: Fri, 20 May 2016 08:40:28 -0400 Subject: [PATCH 340/569] Handle odd sized returns I've noticed many invalid returns from can of size 19 when padded with the additional 5 zeros. This crashes the watcher thread. This will handle them as unhexlify dumps --- obd/protocols/protocol_can.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index de2b1319..069c949e 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -64,6 +64,11 @@ def parse_frame(self, frame): if self.id_bits == 11: raw = "00000" + raw + # Handle odd size frames and drop + if len(raw) > 16 and len(raw) & 1: + debug("Dropping frame for being wrong size (odd)") + return False + raw_bytes = bytearray(unhexlify(raw)) # check for valid size From d0b6ae625be385e9fb793ba04124b666bcd690b4 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 16 Jun 2016 14:02:38 -0400 Subject: [PATCH 341/569] disallow all odd size frames, added test for this --- obd/protocols/protocol_can.py | 6 +++--- obd/protocols/protocol_legacy.py | 5 +++++ tests/test_protocol_can.py | 4 ++++ tests/test_protocol_legacy.py | 4 ++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 069c949e..1ac45e2d 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -65,10 +65,10 @@ def parse_frame(self, frame): raw = "00000" + raw # Handle odd size frames and drop - if len(raw) > 16 and len(raw) & 1: - debug("Dropping frame for being wrong size (odd)") + if len(raw) & 1: + debug("Dropping frame for being odd") return False - + raw_bytes = bytearray(unhexlify(raw)) # check for valid size diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 82599303..22abb137 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -47,6 +47,11 @@ def parse_frame(self, frame): raw = frame.raw + # Handle odd size frames and drop + if len(raw) & 1: + debug("Dropping frame for being odd") + return False + raw_bytes = bytearray(unhexlify(raw)) if len(raw_bytes) < 6: diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index 222ec2c6..5dd1174f 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -40,6 +40,10 @@ def test_single_frame(): # TODO: check for invalid length filterring + # drop odd-sized frames (post padding) + r = p(["7E8 08 41 00 00 01 02 03 04 0"]) + assert len(r) == 0 + def test_hex_straining(): """ diff --git a/tests/test_protocol_legacy.py b/tests/test_protocol_legacy.py index 37631f71..da74c882 100644 --- a/tests/test_protocol_legacy.py +++ b/tests/test_protocol_legacy.py @@ -42,6 +42,10 @@ def test_single_frame(): r = p(["48 6B 10 41 00 00 01 02 03 04 05 FF"]) assert len(r) == 0 + # odd (invalid) + r = p(["48 6B 10 41 00 00 F"]) + assert len(r) == 0 + def test_hex_straining(): """ From 00787d10eabb657451697048db79876d826319a9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 23 Jun 2016 22:05:45 -0400 Subject: [PATCH 342/569] added tests for CAN length checking and zero data --- tests/test_protocol_can.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index 5dd1174f..ccd80730 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -31,14 +31,29 @@ def test_single_frame(): r = p(["7E8 06 41 00 00 01 02 03"]) assert len(r) == 1 - check_message(r[0], 1, 0x0, list(range(4))) + check_message(r[0], 1, 0x0, [0x00, 0x01, 0x02, 0x03]) + # minimum valid length + r = p(["7E8 01 41"]) + assert len(r) == 1 + check_message(r[0], 1, 0x0, []) - r = p(["7E8 08 41 00 00 01 02 03 04 05"]) + # maximum valid length + r = p(["7E8 07 41 00 00 01 02 03 04"]) assert len(r) == 1 - check_message(r[0], 1, 0x0, list(range(6))) + check_message(r[0], 1, 0x0, [0x00, 0x01, 0x02, 0x03, 0x04]) + + # to short + r = p(["7E8"]) + assert len(r) == 0 - # TODO: check for invalid length filterring + # to long + r = p(["7E8 08 41 00 00 01 02 03 04 05"]) + assert len(r) == 0 + + # drop frames with zero data + r = p(["7E8 00"]) + assert len(r) == 0 # drop odd-sized frames (post padding) r = p(["7E8 08 41 00 00 01 02 03 04 0"]) From fe5740d992a83c8cb2430290a1698479fdec9f89 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 23 Jun 2016 22:08:08 -0400 Subject: [PATCH 343/569] check length on CAN responses --- obd/protocols/protocol_can.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 1ac45e2d..d3d8e53c 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -73,15 +73,13 @@ def parse_frame(self, frame): # check for valid size - # TODO: lookup this limit - # if len(raw_bytes) < 9: - # debug("Dropped frame for being too short") - # return False + if len(raw_bytes) < 5: + debug("Dropped frame for being too short") + return False - # TODO: lookup this limit - # if len(raw_bytes) > 16: - # debug("Dropped frame for being too long") - # return False + if len(raw_bytes) > 12: + debug("Dropped frame for being too long") + return False # read header information @@ -133,14 +131,26 @@ def parse_frame(self, frame): # v # 00 00 07 E8 06 41 00 BE 7F B8 13 frame.data_len = frame.data[0] & 0x0F + + # drop frames with no data + if frame.data_len == 0: + return False + elif frame.type == self.FRAME_TYPE_FF: # First frames have 12 bit length codes - # v - # 00 00 07 E8 06 41 00 BE 7F B8 13 + # v vv + # 00 00 07 E8 10 20 49 04 00 01 02 03 frame.data_len = (frame.data[0] & 0x0F) << 8 frame.data_len += frame.data[1] + + # drop frames with no data + if frame.data_len == 0: + return False + elif frame.type == self.FRAME_TYPE_CF: # Consecutive frames have 4 bit sequence indices + # v + # 00 00 07 E8 21 04 05 06 07 08 09 0A frame.seq_index = frame.data[0] & 0x0F return True From fcfc9c9418efdf84badd947922f6f4f0420ccc17 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 28 Jun 2016 22:09:45 -0400 Subject: [PATCH 344/569] removed sketchy end-to-end test, using obdsim now --- tests/test_end_to_end.py | 86 ---------------------------------------- 1 file changed, 86 deletions(-) delete mode 100644 tests/test_end_to_end.py diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py deleted file mode 100644 index afc537c2..00000000 --- a/tests/test_end_to_end.py +++ /dev/null @@ -1,86 +0,0 @@ - -""" -# TODO: rewrite for new protocol architecture -def test_query(): - # we don't need an actual serial connection - o = obd.OBD("/dev/null") - # forge our own command, to control the output - cmd = OBDCommand("TEST", "Test command", "0123", 2, noop, False) - - # forge IO from the car by overwriting the read/write functions - - # buffers - toCar = [""] # needs to be inside mutable object to allow assignment in closure - fromCar = "" - - def write(cmd): - toCar[0] = cmd - - o.is_connected = lambda *args: True - o.port.is_connected = lambda *args: True - o.port._ELM327__status = OBDStatus.CAR_CONNECTED - o.port._ELM327__protocol = SAE_J1850_PWM([]) - o.port._ELM327__primary_ecu = 0x10 - o.port._ELM327__write = write - o.port._ELM327__read = lambda *args: fromCar - - # make sure unsupported commands don't write ------------------------------ - fromCar = ["48 6B 10 41 23 AB CD 10"] - r = o.query(cmd) - assert toCar[0] == "" - assert r.is_null() - - # a correct command transaction ------------------------------------------- - fromCar = ["48 6B 10 41 23 AB CD 10"] # preset the response - r = o.query(cmd, force=True) # run - assert toCar[0] == "0123" # verify that the command was sent correctly - assert not r.is_null() - assert r.value == "ABCD" # verify that the response was parsed correctly - - # response of greater length ---------------------------------------------- - fromCar = ["48 6B 10 41 23 AB CD EF 10"] - r = o.query(cmd, force=True) - assert toCar[0] == "0123" - assert r.value == "ABCD" - - # response of lesser length ----------------------------------------------- - fromCar = ["48 6B 10 41 23 AB 10"] - r = o.query(cmd, force=True) - assert toCar[0] == "0123" - assert r.value == "AB00" - - # NO DATA response -------------------------------------------------------- - fromCar = ["NO DATA"] - r = o.query(cmd, force=True) - assert r.is_null() - - # malformed response ------------------------------------------------------ - fromCar = ["totaly not hex!@#$"] - r = o.query(cmd, force=True) - assert r.is_null() - - # no response ------------------------------------------------------------- - fromCar = [""] - r = o.query(cmd, force=True) - assert r.is_null() - - # reject responses from other ECUs --------------------------------------- - fromCar = ["48 6B 12 41 23 AB CD 10"] - r = o.query(cmd, force=True) - assert toCar[0] == "0123" - assert r.is_null() - - # filter for primary ECU -------------------------------------------------- - fromCar = ["48 6B 12 41 23 AB CD 10", "48 6B 10 41 23 AB CD 10"] - r = o.query(cmd, force=True) - assert toCar[0] == "0123" - assert r.value == "ABCD" - - ''' - # ignore multiline responses ---------------------------------------------- - fromCar = ["48 6B 10 41 23 AB CD 10", "48 6B 10 41 23 AB CD 10"] - r = o.query(cmd, force=True) - assert toCar[0] == "0123" - assert r.is_null() - ''' -""" From a67a945dcdc3de90b91387322e766f71e1cf2dd8 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 28 Jun 2016 22:22:24 -0400 Subject: [PATCH 345/569] bumped up the min CAN frame size to allow for 12 bit PCI lengths --- obd/protocols/protocol_can.py | 7 ++++++- tests/test_protocol_can.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index d3d8e53c..f02aa35c 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -73,7 +73,12 @@ def parse_frame(self, frame): # check for valid size - if len(raw_bytes) < 5: + if len(raw_bytes) < 6: + # make sure that we have at least a PCI byte, and one following byte + # for FF frames with 12-bit length codes, or 1 byte of data + # + # 00 00 07 E8 10 20 ... + debug("Dropped frame for being too short") return False diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index ccd80730..cfa10789 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -44,7 +44,7 @@ def test_single_frame(): check_message(r[0], 1, 0x0, [0x00, 0x01, 0x02, 0x03, 0x04]) # to short - r = p(["7E8"]) + r = p(["7E8 01"]) assert len(r) == 0 # to long From 350e826b28d41e1c8eb0c4ac2540a213478e08c2 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Apr 2016 13:22:45 -0400 Subject: [PATCH 346/569] using byte literals for commands --- obd/commands.py | 215 +++++++++++++++++++++++++----------------------- obd/elm327.py | 28 +++---- obd/obd.py | 4 +- 3 files changed, 128 insertions(+), 119 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index e166d4a5..1789f3dd 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -48,106 +48,106 @@ __mode1__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "0100", 4, pid, ECU.ENGINE, True), - OBDCommand("STATUS" , "Status since DTCs cleared" , "0101", 4, status, ECU.ENGINE, True), - OBDCommand("FREEZE_DTC" , "Freeze DTC" , "0102", 2, drop, ECU.ENGINE, True), - OBDCommand("FUEL_STATUS" , "Fuel System Status" , "0103", 2, fuel_status, ECU.ENGINE, True), - OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , "0104", 1, percent, ECU.ENGINE, True), - OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "0105", 1, temp, ECU.ENGINE, True), - OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "0106", 1, percent_centered, ECU.ENGINE, True), - OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "0107", 1, percent_centered, ECU.ENGINE, True), - OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "0108", 1, percent_centered, ECU.ENGINE, True), - OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "0109", 1, percent_centered, ECU.ENGINE, True), - OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "010A", 1, fuel_pressure, ECU.ENGINE, True), - OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "010B", 1, pressure, ECU.ENGINE, True), - OBDCommand("RPM" , "Engine RPM" , "010C", 2, rpm, ECU.ENGINE, True), - OBDCommand("SPEED" , "Vehicle Speed" , "010D", 1, speed, ECU.ENGINE, True), - OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "010E", 1, timing_advance, ECU.ENGINE, True), - OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "010F", 1, temp, ECU.ENGINE, True), - OBDCommand("MAF" , "Air Flow Rate (MAF)" , "0110", 2, maf, ECU.ENGINE, True), - OBDCommand("THROTTLE_POS" , "Throttle Position" , "0111", 1, percent, ECU.ENGINE, True), - OBDCommand("AIR_STATUS" , "Secondary Air Status" , "0112", 1, air_status, ECU.ENGINE, True), - OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "0113", 1, drop, ECU.ENGINE, True), - OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , "0114", 2, sensor_voltage, ECU.ENGINE, True), - OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , "0115", 2, sensor_voltage, ECU.ENGINE, True), - OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , "0116", 2, sensor_voltage, ECU.ENGINE, True), - OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , "0117", 2, sensor_voltage, ECU.ENGINE, True), - OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , "0118", 2, sensor_voltage, ECU.ENGINE, True), - OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , "0119", 2, sensor_voltage, ECU.ENGINE, True), - OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , "011A", 2, sensor_voltage, ECU.ENGINE, True), - OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , "011B", 2, sensor_voltage, ECU.ENGINE, True), - OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "011C", 1, obd_compliance, ECU.ENGINE, True), - OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "011D", 1, drop, ECU.ENGINE, True), - OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "011E", 1, drop, ECU.ENGINE, True), - OBDCommand("RUN_TIME" , "Engine Run Time" , "011F", 2, seconds, ECU.ENGINE, True), + OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , b"0100", 4, pid, ECU.ENGINE, True), + OBDCommand("STATUS" , "Status since DTCs cleared" , b"0101", 4, status, ECU.ENGINE, True), + OBDCommand("FREEZE_DTC" , "Freeze DTC" , b"0102", 2, drop, ECU.ENGINE, True), + OBDCommand("FUEL_STATUS" , "Fuel System Status" , b"0103", 2, fuel_status, ECU.ENGINE, True), + OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , b"0104", 1, percent, ECU.ENGINE, True), + OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , b"0105", 1, temp, ECU.ENGINE, True), + OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , b"0106", 1, percent_centered, ECU.ENGINE, True), + OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , b"0107", 1, percent_centered, ECU.ENGINE, True), + OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , b"0108", 1, percent_centered, ECU.ENGINE, True), + OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , b"0109", 1, percent_centered, ECU.ENGINE, True), + OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , b"010A", 1, fuel_pressure, ECU.ENGINE, True), + OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , b"010B", 1, pressure, ECU.ENGINE, True), + OBDCommand("RPM" , "Engine RPM" , b"010C", 2, rpm, ECU.ENGINE, True), + OBDCommand("SPEED" , "Vehicle Speed" , b"010D", 1, speed, ECU.ENGINE, True), + OBDCommand("TIMING_ADVANCE" , "Timing Advance" , b"010E", 1, timing_advance, ECU.ENGINE, True), + OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , b"010F", 1, temp, ECU.ENGINE, True), + OBDCommand("MAF" , "Air Flow Rate (MAF)" , b"0110", 2, maf, ECU.ENGINE, True), + OBDCommand("THROTTLE_POS" , "Throttle Position" , b"0111", 1, percent, ECU.ENGINE, True), + OBDCommand("AIR_STATUS" , "Secondary Air Status" , b"0112", 1, air_status, ECU.ENGINE, True), + OBDCommand("O2_SENSORS" , "O2 Sensors Present" , b"0113", 1, drop, ECU.ENGINE, True), + OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , b"0114", 2, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , b"0115", 2, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , b"0116", 2, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , b"0117", 2, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , b"0118", 2, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , b"0119", 2, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , b"011A", 2, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , b"011B", 2, sensor_voltage, ECU.ENGINE, True), + OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , b"011C", 1, obd_compliance, ECU.ENGINE, True), + OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , b"011D", 1, drop, ECU.ENGINE, True), + OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , b"011E", 1, drop, ECU.ENGINE, True), + OBDCommand("RUN_TIME" , "Engine Run Time" , b"011F", 2, seconds, ECU.ENGINE, True), # name description cmd bytes decoder ECU fast - OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "0120", 4, pid, ECU.ENGINE, True), - OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , "0121", 2, distance, ECU.ENGINE, True), - OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , "0122", 2, fuel_pres_vac, ECU.ENGINE, True), - OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , "0123", 2, fuel_pres_direct, ECU.ENGINE, True), - OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , "0124", 4, sensor_voltage_big, ECU.ENGINE, True), - OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , "0125", 4, sensor_voltage_big, ECU.ENGINE, True), - OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , "0126", 4, sensor_voltage_big, ECU.ENGINE, True), - OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , "0127", 4, sensor_voltage_big, ECU.ENGINE, True), - OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , "0128", 4, sensor_voltage_big, ECU.ENGINE, True), - OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , "0129", 4, sensor_voltage_big, ECU.ENGINE, True), - OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , "012A", 4, sensor_voltage_big, ECU.ENGINE, True), - OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , "012B", 4, sensor_voltage_big, ECU.ENGINE, True), - OBDCommand("COMMANDED_EGR" , "Commanded EGR" , "012C", 1, percent, ECU.ENGINE, True), - OBDCommand("EGR_ERROR" , "EGR Error" , "012D", 1, percent_centered, ECU.ENGINE, True), - OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , "012E", 1, percent, ECU.ENGINE, True), - OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , "012F", 1, percent, ECU.ENGINE, True), - OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , "0130", 1, count, ECU.ENGINE, True), - OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , "0131", 2, distance, ECU.ENGINE, True), - OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , "0132", 2, evap_pressure, ECU.ENGINE, True), - OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , "0133", 1, pressure, ECU.ENGINE, True), - OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , "0134", 4, current_centered, ECU.ENGINE, True), - OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , "0135", 4, current_centered, ECU.ENGINE, True), - OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , "0136", 4, current_centered, ECU.ENGINE, True), - OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , "0137", 4, current_centered, ECU.ENGINE, True), - OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , "0138", 4, current_centered, ECU.ENGINE, True), - OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , "0139", 4, current_centered, ECU.ENGINE, True), - OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , "013A", 4, current_centered, ECU.ENGINE, True), - OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , "013B", 4, current_centered, ECU.ENGINE, True), - OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , "013C", 2, catalyst_temp, ECU.ENGINE, True), - OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , "013D", 2, catalyst_temp, ECU.ENGINE, True), - OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , "013E", 2, catalyst_temp, ECU.ENGINE, True), - OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , "013F", 2, catalyst_temp, ECU.ENGINE, True), + OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , b"0120", 4, pid, ECU.ENGINE, True), + OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , b"0121", 2, distance, ECU.ENGINE, True), + OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , b"0122", 2, fuel_pres_vac, ECU.ENGINE, True), + OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , b"0123", 2, fuel_pres_direct, ECU.ENGINE, True), + OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , b"0124", 4, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , b"0125", 4, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , b"0126", 4, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , b"0127", 4, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , b"0128", 4, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , b"0129", 4, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , b"012A", 4, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , b"012B", 4, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("COMMANDED_EGR" , "Commanded EGR" , b"012C", 1, percent, ECU.ENGINE, True), + OBDCommand("EGR_ERROR" , "EGR Error" , b"012D", 1, percent_centered, ECU.ENGINE, True), + OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , b"012E", 1, percent, ECU.ENGINE, True), + OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , b"012F", 1, percent, ECU.ENGINE, True), + OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , b"0130", 1, count, ECU.ENGINE, True), + OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , b"0131", 2, distance, ECU.ENGINE, True), + OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , b"0132", 2, evap_pressure, ECU.ENGINE, True), + OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , b"0133", 1, pressure, ECU.ENGINE, True), + OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , b"0134", 4, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , b"0135", 4, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , b"0136", 4, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , b"0137", 4, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , b"0138", 4, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , b"0139", 4, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , b"013A", 4, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , b"013B", 4, current_centered, ECU.ENGINE, True), + OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , b"013C", 2, catalyst_temp, ECU.ENGINE, True), + OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , b"013D", 2, catalyst_temp, ECU.ENGINE, True), + OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , b"013E", 2, catalyst_temp, ECU.ENGINE, True), + OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , b"013F", 2, catalyst_temp, ECU.ENGINE, True), # name description cmd bytes decoder ECU fast - OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "0140", 4, pid, ECU.ENGINE, True), - OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "0141", 4, drop, ECU.ENGINE, True), - OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "0142", 2, drop, ECU.ENGINE, True), - OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "0143", 2, drop, ECU.ENGINE, True), - OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "0144", 2, drop, ECU.ENGINE, True), - OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , "0145", 1, percent, ECU.ENGINE, True), - OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , "0146", 1, temp, ECU.ENGINE, True), - OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , "0147", 1, percent, ECU.ENGINE, True), - OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , "0148", 1, percent, ECU.ENGINE, True), - OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , "0149", 1, percent, ECU.ENGINE, True), - OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , "014A", 1, percent, ECU.ENGINE, True), - OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , "014B", 1, percent, ECU.ENGINE, True), - OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , "014C", 1, percent, ECU.ENGINE, True), - OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , "014D", 2, minutes, ECU.ENGINE, True), - OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , "014E", 2, minutes, ECU.ENGINE, True), - OBDCommand("MAX_VALUES" , "Various Max values" , "014F", 4, drop, ECU.ENGINE, True), # todo: decode this - OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , "0150", 4, max_maf, ECU.ENGINE, True), - OBDCommand("FUEL_TYPE" , "Fuel Type" , "0151", 1, fuel_type, ECU.ENGINE, True), - OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , "0152", 1, percent, ECU.ENGINE, True), - OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , "0153", 2, abs_evap_pressure, ECU.ENGINE, True), - OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , "0154", 2, evap_pressure_alt, ECU.ENGINE, True), - OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , "0155", 2, percent_centered, ECU.ENGINE, True), # todo: decode seconds value for banks 3 and 4 - OBDCommand("LONG_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , "0156", 2, percent_centered, ECU.ENGINE, True), - OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , "0157", 2, percent_centered, ECU.ENGINE, True), - OBDCommand("LONG_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , "0158", 2, percent_centered, ECU.ENGINE, True), - OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , "0159", 2, fuel_pres_direct, ECU.ENGINE, True), - OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , "015A", 1, percent, ECU.ENGINE, True), - OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , "015B", 1, percent, ECU.ENGINE, True), - OBDCommand("OIL_TEMP" , "Engine oil temperature" , "015C", 1, temp, ECU.ENGINE, True), - OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , "015D", 2, inject_timing, ECU.ENGINE, True), - OBDCommand("FUEL_RATE" , "Engine fuel rate" , "015E", 2, fuel_rate, ECU.ENGINE, True), - OBDCommand("EMISSION_REQ" , "Designed emission requirements" , "015F", 1, drop, ECU.ENGINE, True), + OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , b"0140", 4, pid, ECU.ENGINE, True), + OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , b"0141", 4, drop, ECU.ENGINE, True), + OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , b"0142", 2, drop, ECU.ENGINE, True), + OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , b"0143", 2, drop, ECU.ENGINE, True), + OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , b"0144", 2, drop, ECU.ENGINE, True), + OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , b"0145", 1, percent, ECU.ENGINE, True), + OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , b"0146", 1, temp, ECU.ENGINE, True), + OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , b"0147", 1, percent, ECU.ENGINE, True), + OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , b"0148", 1, percent, ECU.ENGINE, True), + OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , b"0149", 1, percent, ECU.ENGINE, True), + OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , b"014A", 1, percent, ECU.ENGINE, True), + OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , b"014B", 1, percent, ECU.ENGINE, True), + OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , b"014C", 1, percent, ECU.ENGINE, True), + OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , b"014D", 2, minutes, ECU.ENGINE, True), + OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , b"014E", 2, minutes, ECU.ENGINE, True), + OBDCommand("MAX_VALUES" , "Various Max values" , b"014F", 4, drop, ECU.ENGINE, True), # todo: decode this + OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , b"0150", 4, max_maf, ECU.ENGINE, True), + OBDCommand("FUEL_TYPE" , "Fuel Type" , b"0151", 1, fuel_type, ECU.ENGINE, True), + OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , b"0152", 1, percent, ECU.ENGINE, True), + OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , b"0153", 2, abs_evap_pressure, ECU.ENGINE, True), + OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , b"0154", 2, evap_pressure_alt, ECU.ENGINE, True), + OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , b"0155", 2, percent_centered, ECU.ENGINE, True), # todo: decode seconds value for banks 3 and 4 + OBDCommand("LONG_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , b"0156", 2, percent_centered, ECU.ENGINE, True), + OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , b"0157", 2, percent_centered, ECU.ENGINE, True), + OBDCommand("LONG_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , b"0158", 2, percent_centered, ECU.ENGINE, True), + OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , b"0159", 2, fuel_pres_direct, ECU.ENGINE, True), + OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , b"015A", 1, percent, ECU.ENGINE, True), + OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , b"015B", 1, percent, ECU.ENGINE, True), + OBDCommand("OIL_TEMP" , "Engine oil temperature" , b"015C", 1, temp, ECU.ENGINE, True), + OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , b"015D", 2, inject_timing, ECU.ENGINE, True), + OBDCommand("FUEL_RATE" , "Engine fuel rate" , b"015E", 2, fuel_rate, ECU.ENGINE, True), + OBDCommand("EMISSION_REQ" , "Designed emission requirements" , b"015F", 1, drop, ECU.ENGINE, True), ] @@ -155,7 +155,7 @@ __mode2__ = [] for c in __mode1__: c = c.clone() - c.command = "02" + c.command[2:] # change the mode: 0100 ---> 0200 + c.command = b"02" + c.command[2:] # change the mode: 0100 ---> 0200 c.name = "DTC_" + c.name c.desc = "DTC " + c.desc __mode2__.append(c) @@ -163,23 +163,30 @@ __mode3__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("GET_DTC" , "Get DTCs" , "03", 0, dtc, ECU.ALL, False), + OBDCommand("GET_DTC" , "Get DTCs" , b"03", 0, dtc, ECU.ALL, False), ] __mode4__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", 0, drop, ECU.ALL, False), + OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , b"04", 0, drop, ECU.ALL, False), ] __mode7__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", 0, dtc, ECU.ALL, False), + OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , b"07", 0, dtc, ECU.ALL, False), +] + +__mode9__ = [ + # name description cmd bytes decoder ECU fast + OBDCommand("PIDS_9A" , "Supported PIDs [01-20]" , b"0900", 4, pid, ECU.ENGINE, True), + OBDCommand("VIN_MESSAGE_COUNT" , "VIN Message Count" , b"0901", 1, count, ECU.ENGINE, True), + OBDCommand("VIN" , "Get Vehicle Identification Number" , b"0902", 20, raw_string, ECU.ENGINE, True), ] __misc__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("ELM_VERSION" , "ELM327 version string" , "ATI", 0, raw_string, ECU.UNKNOWN, False), - OBDCommand("ELM_VOLTAGE" , "Voltage detected by OBD-II adapter" , "ATRV", 0, elm_voltage, ECU.UNKNOWN, False), + OBDCommand("ELM_VERSION" , "ELM327 version string" , b"ATI", 0, raw_string, ECU.UNKNOWN, False), + OBDCommand("ELM_VOLTAGE" , "Voltage detected by OBD-II adapter" , b"ATRV", 0, elm_voltage, ECU.UNKNOWN, False), ] @@ -200,7 +207,9 @@ def __init__(self): __mode4__, [], [], - __mode7__ + __mode7__, + [], + __mode9__, ] # allow commands to be accessed by name diff --git a/obd/elm327.py b/obd/elm327.py index 0b0841f9..d965ec15 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -115,26 +115,26 @@ def __init__(self, portname, baudrate, protocol): # ---------------------------- ATZ (reset) ---------------------------- try: - self.__send("ATZ", delay=1) # wait 1 second for ELM to initialize + self.__send(b"ATZ", delay=1) # wait 1 second for ELM to initialize # return data can be junk, so don't bother checking except serial.SerialException as e: self.__error(e) return # -------------------------- ATE0 (echo OFF) -------------------------- - r = self.__send("ATE0") + r = self.__send(b"ATE0") if not self.__isok(r, expectEcho=True): self.__error("ATE0 did not return 'OK'") return # ------------------------- ATH1 (headers ON) ------------------------- - r = self.__send("ATH1") + r = self.__send(b"ATH1") if not self.__isok(r): self.__error("ATH1 did not return 'OK', or echoing is still ON") return # ------------------------ ATL0 (linefeeds OFF) ----------------------- - r = self.__send("ATL0") + r = self.__send(b"ATL0") if not self.__isok(r): self.__error("ATL0 did not return 'OK'") return @@ -164,8 +164,8 @@ def load_protocol(self, protocol): def manual_protocol(self, protocol): - r = self.__send("ATTP%s" % protocol) - r0100 = self.__send("0100") + r = self.__send(b"ATTP" + protocol.encode()) + r0100 = self.__send(b"0100") if not self.__has_message(r0100, "UNABLE TO CONNECT"): # success, found the protocol @@ -186,13 +186,13 @@ def auto_protocol(self): """ # -------------- try the ELM's auto protocol mode -------------- - r = self.__send("ATSP0") + r = self.__send(b"ATSP0") # -------------- 0100 (first command, SEARCH protocols) -------------- - r0100 = self.__send("0100") + r0100 = self.__send(b"0100") # ------------------- ATDPN (list protocol number) ------------------- - r = self.__send("ATDPN") + r = self.__send(b"ATDPN") if len(r) != 1: debug("Failed to retrieve current protocol", True) return False @@ -214,8 +214,8 @@ def auto_protocol(self): debug("ELM responded with unknown protocol. Trying them one-by-one") for p in self._TRY_PROTOCOL_ORDER: - r = self.__send("ATTP%s" % p) - r0100 = self.__send("0100") + r = self.__send(b"ATTP" + p.encode()) + r0100 = self.__send(b"0100") if not self.__has_message(r0100, "UNABLE TO CONNECT"): # success, found the protocol self.__protocol = self._SUPPORTED_PROTOCOLS[p](r0100) @@ -287,7 +287,7 @@ def close(self): self.__protocol = None if self.__port is not None: - self.__write("ATZ") + self.__write(b"ATZ") self.__port.close() self.__port = None @@ -337,10 +337,10 @@ def __write(self, cmd): """ if self.__port: - cmd += "\r\n" # terminate + cmd += b"\r\n" # terminate debug("write: " + repr(cmd)) self.__port.flushInput() # dump everything in the input buffer - self.__port.write(cmd.encode()) # turn the string into bytes and write + self.__port.write(cmd) # turn the string into bytes and write self.__port.flush() # wait for the output buffer to finish transmitting else: debug("cannot perform __write() when unconnected", True) diff --git a/obd/obd.py b/obd/obd.py index 14d22580..34ea764b 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -256,10 +256,10 @@ def __build_command_string(self, cmd): # only wait for as many ECUs as we've seen if self.fast and cmd.fast: - cmd_string += str(len(self.port.ecus())) + cmd_string += str(len(self.port.ecus())).encode() # if we sent this last time, just send if self.fast and (cmd_string == self.__last_command): - cmd_string = "" + cmd_string = b"" return cmd_string From d76acea8803cd8d57cd02b7d640ad74abdef7c16 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 16 Jun 2016 13:39:20 -0400 Subject: [PATCH 347/569] started implemention auto baud rate --- obd/elm327.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/obd/elm327.py b/obd/elm327.py index d965ec15..c0c482d2 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -85,6 +85,19 @@ class ELM327: "A", # SAE_J1939 ] + # 38400, 9600 are the possible boot bauds (unless reprogrammed via + # PP 0C). 19200, 38400, 57600, 115200, 230400, 500000 are listed on + # p.46 of the ELM327 datasheet. + # + # Once pyserial supports non-standard baud rates on platforms other + # than Linux, we'll add 500K to this list. + # + # We check the two default baud rates first, then go fastest to + # slowest, on the theory that anyone who's using a slow baud rate is + # going to be less picky about the time required to detect it. + _TRY_BAUDS = [ 38400, 9600, 230400, 115200, 57600, 19200 ] + + def __init__(self, portname, baudrate, protocol): """Initializes port by resetting device and gettings supported PIDs. """ @@ -225,6 +238,35 @@ def auto_protocol(self): return False + def auto_baudrate(self): + """Detect, select, and return the baud rate at which a connected + ELM32x interface is operating. + + Return None if the baud rate couldn't be determined. + """ + for baud in self._TRY_BAUDS: + self.port.setBaudrate(baud) + self.port.flushInput() + self.port.flushOutput() + + # Send a nonsense command to get a prompt back from the scanner + # (an empty command runs the risk of repeating a dangerous command) + # The first character might get eaten if the interface was busy, + # so write a second one (again so that the lone CR doesn't repeat + # the previous command) + port.write("\x7F\x7F\r") + port.set_timeout(timeout) + response = self.__read() + + if (response.endswith("\r\r>")): + #print "%d baud detected (%r)" % (baud, response) + break + else: + baud = None + + return baud + + def __isok(self, lines, expectEcho=False): if not lines: From 3e96a65ee534f1bd27542e644ecfd73c0eda96fe Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 28 Jun 2016 23:54:57 -0400 Subject: [PATCH 348/569] first test with python native logging tools --- obd/__init__.py | 9 +++++++++ obd/obd.py | 33 ++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/obd/__init__.py b/obd/__init__.py index 51ea205c..866cbdb0 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -45,3 +45,12 @@ from .protocols import ECU from .utils import scan_serial, scanSerial, OBDStatus # TODO: scanSerial() deprecated from .debug import debug + +import logging + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +console_handler = logging.StreamHandler() # sends output to stderr +console_handler.setFormatter(logging.Formatter("[%(name)s] %(message)s")) +logger.addHandler(console_handler) diff --git a/obd/obd.py b/obd/obd.py index 34ea764b..3811b7b6 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -30,6 +30,8 @@ ######################################################################## +import logging + from .__version__ import __version__ from .elm327 import ELM327 from .commands import commands @@ -37,6 +39,7 @@ from .utils import scan_serial, OBDStatus from .debug import debug +logger = logging.getLogger(__name__) class OBD(object): @@ -51,10 +54,10 @@ def __init__(self, portstr=None, baudrate=38400, protocol=None, fast=True): self.fast = fast self.__last_command = "" # used for running the previous command with a CR - debug("========================== python-OBD (v%s) ==========================" % __version__) + logger.info("======================= python-OBD (v%s) =======================" % __version__) self.__connect(portstr, baudrate, protocol) # initialize by connecting and loading sensors self.__load_commands() # try to load the car's supported commands - debug("=========================================================================") + logger.info("===================================================================") def __connect(self, portstr, baudrate, protocol): @@ -63,22 +66,22 @@ def __connect(self, portstr, baudrate, protocol): """ if portstr is None: - debug("Using scan_serial to select port") + logger.info("Using scan_serial to select port") portnames = scan_serial() - debug("Available ports: " + str(portnames)) + logger.info("Available ports: " + str(portnames)) if not portnames: - debug("No OBD-II adapters found", True) + logger.warning("No OBD-II adapters found") return for port in portnames: - debug("Attempting to use port: " + str(port)) + logger.info("Attempting to use port: " + str(port)) self.port = ELM327(port, baudrate, protocol) if self.port.status() >= OBDStatus.ELM_CONNECTED: break # success! stop searching for serial else: - debug("Explicit port defined") + logger.info("Explicit port defined") self.port = ELM327(portstr, baudrate, protocol) # if the connection failed, close it @@ -94,10 +97,10 @@ def __load_commands(self): """ if self.status() != OBDStatus.CAR_CONNECTED: - debug("Cannot load commands: No connection to car", True) + logger.warning("Cannot load commands: No connection to car") return - debug("querying for supported PIDs (commands)...") + logger.info("querying for supported PIDs (commands)...") pid_getters = commands.pid_getters() for get in pid_getters: # PID listing commands should sequentialy become supported @@ -128,7 +131,7 @@ def __load_commands(self): if mode == 1 and commands.has_pid(2, pid): self.supported_commands.add(commands[2][pid]) - debug("finished querying with %d commands supported" % len(self.supported_commands)) + logger.info("finished querying with %d commands supported" % len(self.supported_commands)) def close(self): @@ -139,7 +142,7 @@ def close(self): self.supported_commands = [] if self.port is not None: - debug("Closing connection") + logger.info("Closing connection") self.port.close() self.port = None @@ -226,16 +229,16 @@ def query(self, cmd, force=False): """ if self.status() == OBDStatus.NOT_CONNECTED: - debug("Query failed, no connection available", True) + logger.warning("Query failed, no connection available") return OBDResponse() if not force and not self.supports(cmd): - debug("'%s' is not supported" % str(cmd), True) + logger.warning("'%s' is not supported" % str(cmd)) return OBDResponse() # send command and retrieve message - debug("Sending command: %s" % str(cmd)) + logger.info("Sending command: %s" % str(cmd)) cmd_string = self.__build_command_string(cmd) messages = self.port.send_and_parse(cmd_string) @@ -244,7 +247,7 @@ def query(self, cmd, force=False): self.__last_command = cmd_string if not messages: - debug("No valid OBD Messages returned", True) + logger.info("No valid OBD Messages returned") return OBDResponse() return cmd(messages) # compute a response object From 9a2241f6dfc3a31b8cd116859c0f2d3a970fddc5 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 28 Jun 2016 23:55:28 -0400 Subject: [PATCH 349/569] fixed indentation error in new baud rate code --- obd/elm327.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/elm327.py b/obd/elm327.py index c0c482d2..a8faa899 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -256,7 +256,7 @@ def auto_baudrate(self): # the previous command) port.write("\x7F\x7F\r") port.set_timeout(timeout) - response = self.__read() + response = self.__read() if (response.endswith("\r\r>")): #print "%d baud detected (%r)" % (baud, response) From bb4ade468417f885e3df3ee0f07ef50268477d37 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 00:03:02 -0400 Subject: [PATCH 350/569] added new logging to elm327.py --- obd/__init__.py | 1 - obd/elm327.py | 39 ++++++++++++++++++++------------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/obd/__init__.py b/obd/__init__.py index 866cbdb0..582956c9 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -44,7 +44,6 @@ from .OBDResponse import OBDResponse, Unit from .protocols import ECU from .utils import scan_serial, scanSerial, OBDStatus # TODO: scanSerial() deprecated -from .debug import debug import logging diff --git a/obd/elm327.py b/obd/elm327.py index a8faa899..bdad95d0 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -32,10 +32,11 @@ import re import serial import time +import logging from .protocols import * from .utils import OBDStatus -from .debug import debug +logger = logging.getLogger(__name__) class ELM327: @@ -109,14 +110,14 @@ def __init__(self, portname, baudrate, protocol): # ------------- open port ------------- try: - debug("Opening serial port '%s'" % portname) + logger.info("Opening serial port '%s'" % portname) self.__port = serial.Serial(portname, \ baudrate = baudrate, \ parity = serial.PARITY_NONE, \ stopbits = 1, \ bytesize = 8, \ timeout = 3) # seconds - debug("Serial port successfully opened on " + self.port_name()) + logger.info("Serial port successfully opened on " + self.port_name()) except serial.SerialException as e: self.__error(e) @@ -158,16 +159,16 @@ def __init__(self, portname, baudrate, protocol): # try to communicate with the car, and load the correct protocol parser if self.load_protocol(protocol): self.__status = OBDStatus.CAR_CONNECTED - debug("Connection successful") + logger.info("Connection successful") else: - debug("Connected to the adapter, but failed to connect to the vehicle", True) + logger.error("Connected to the adapter, but failed to connect to the vehicle") def load_protocol(self, protocol): if protocol is not None: # an explicit protocol was specified if protocol not in self._SUPPORTED_PROTOCOLS: - debug("%s is not a valid protocol. Please use \"1\" through \"A\"", True) + logger.error("%s is not a valid protocol. Please use \"1\" through \"A\"") return False return self.manual_protocol(protocol) else: @@ -207,7 +208,7 @@ def auto_protocol(self): # ------------------- ATDPN (list protocol number) ------------------- r = self.__send(b"ATDPN") if len(r) != 1: - debug("Failed to retrieve current protocol", True) + logger.error("Failed to retrieve current protocol") return False @@ -224,7 +225,7 @@ def auto_protocol(self): # an unknown protocol # this is likely because not all adapter/car combinations work # in "auto" mode. Some respond to ATDPN responded with "0" - debug("ELM responded with unknown protocol. Trying them one-by-one") + logger.info("ELM responded with unknown protocol. Trying them one-by-one") for p in self._TRY_PROTOCOL_ORDER: r = self.__send(b"ATTP" + p.encode()) @@ -287,13 +288,13 @@ def __has_message(self, lines, text): def __error(self, msg=None): - """ handles fatal failures, print debug info and closes serial """ + """ handles fatal failures, print logger.info info and closes serial """ self.close() - debug("Connection Error:", True) + logger.error("Connection Error:") if msg is not None: - debug(' ' + str(msg), True) + logger.error(str(msg)) def port_name(self): @@ -347,7 +348,7 @@ def send_and_parse(self, cmd): """ if self.__status == OBDStatus.NOT_CONNECTED: - debug("cannot send_and_parse() when unconnected", True) + logger.info("cannot send_and_parse() when unconnected") return None lines = self.__send(cmd) @@ -367,7 +368,7 @@ def __send(self, cmd, delay=None): self.__write(cmd) if delay is not None: - debug("wait: %d seconds" % delay) + logger.info("wait: %d seconds" % delay) time.sleep(delay) return self.__read() @@ -380,12 +381,12 @@ def __write(self, cmd): if self.__port: cmd += b"\r\n" # terminate - debug("write: " + repr(cmd)) + logger.debug("write: " + repr(cmd)) self.__port.flushInput() # dump everything in the input buffer self.__port.write(cmd) # turn the string into bytes and write self.__port.flush() # wait for the output buffer to finish transmitting else: - debug("cannot perform __write() when unconnected", True) + logger.info("cannot perform __write() when unconnected") def __read(self): @@ -407,10 +408,10 @@ def __read(self): if not c: if attempts <= 0: - debug("Failed to read port, giving up") + logger.info("Failed to read port, giving up") break - debug("Failed to read port, trying again...") + logger.info("Failed to read port, trying again...") attempts -= 1 continue @@ -424,10 +425,10 @@ def __read(self): buffer += c # whatever is left must be part of the response else: - debug("cannot perform __read() when unconnected", True) + logger.info("cannot perform __read() when unconnected") return "" - debug("read: " + repr(buffer)) + logger.debug("read: " + repr(buffer)) # convert bytes into a standard string raw = buffer.decode() From b91d9d56b9e93b4c7a32dd5d70073b6ab415e9dd Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 00:23:11 -0400 Subject: [PATCH 351/569] added logging to remaining files --- obd/OBDCommand.py | 11 +++++++++-- obd/async.py | 33 ++++++++++++++++++--------------- obd/commands.py | 14 ++++++++------ obd/decoders.py | 26 +++++++++++++++----------- obd/obd.py | 3 +-- obd/utils.py | 7 +++++-- 6 files changed, 56 insertions(+), 38 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 65a365e5..967ec9ec 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -30,10 +30,13 @@ ######################################################################## from .utils import * -from .debug import debug from .protocols import ECU from .OBDResponse import OBDResponse +import logging + +logger = logging.getLogger(__name__) + class OBDCommand(): def __init__(self, @@ -78,7 +81,7 @@ def pid_int(self): # TODO: remove later @property def supported(self): - debug("OBDCommand.supported is deprecated. Use OBD.supports() instead", True) + logger.warning("OBDCommand.supported is deprecated. Use OBD.supports() instead") return False def __call__(self, messages): @@ -96,6 +99,8 @@ def __call__(self, messages): r = OBDResponse(self, messages) if messages: r.value, r.unit = self.decode(messages) + else: + logger.info(str(self) + " did not recieve any acceptable messages") return r @@ -106,9 +111,11 @@ def __constrain_message_data(self, message): if len(message.data) > self.bytes: # chop off the right side message.data = message.data[:self.bytes] + logger.debug("Message was longer than expected. Trimmed message: " + repr(message.data)) else: # pad the right with zeros message.data += (b'\x00' * (self.bytes - len(message.data))) + logger.debug("Message was shorter than expected. Padded message: " + repr(message.data)) def __str__(self): diff --git a/obd/async.py b/obd/async.py index 264600da..9ea67b40 100644 --- a/obd/async.py +++ b/obd/async.py @@ -31,9 +31,12 @@ import time import threading +import logging from .OBDResponse import OBDResponse -from .debug import debug -from . import OBD +from .obd import OBD + +logger = logging.getLogger(__name__) + class Async(OBD): """ @@ -58,15 +61,15 @@ def running(self): def start(self): """ Starts the async update loop """ if not self.is_connected(): - debug("Async thread not started because no connection was made") + logger.info("Async thread not started because no connection was made") return if len(self.__commands) == 0: - debug("Async thread not started because no commands were registered") + logger.info("Async thread not started because no commands were registered") return if self.__thread is None: - debug("Starting async thread") + logger.info("Starting async thread") self.__running = True self.__thread = threading.Thread(target=self.run) self.__thread.daemon = True @@ -76,11 +79,11 @@ def start(self): def stop(self): """ Stops the async update loop """ if self.__thread is not None: - debug("Stopping async thread...") + logger.info("Stopping async thread...") self.__running = False self.__thread.join() self.__thread = None - debug("Async thread stopped") + logger.info("Async thread stopped") def paused(self): @@ -130,22 +133,22 @@ def watch(self, c, callback=None, force=False): # the dict shouldn't be changed while the daemon thread is iterating if self.__running: - debug("Can't watch() while running, please use stop()", True) + logger.warning("Can't watch() while running, please use stop()") else: if not force and not self.supports(c): - debug("'%s' is not supported" % str(c), True) + logger.warning("'%s' is not supported" % str(c)) return # new command being watched, store the command if c not in self.__commands: - debug("Watching command: %s" % str(c)) + logger.info("Watching command: %s" % str(c)) self.__commands[c] = OBDResponse() # give it an initial value self.__callbacks[c] = [] # create an empty list # if a callback was given, push it if hasattr(callback, "__call__") and (callback not in self.__callbacks[c]): - debug("subscribing callback for command: %s" % str(c)) + logger.info("subscribing callback for command: %s" % str(c)) self.__callbacks[c].append(callback) @@ -158,9 +161,9 @@ def unwatch(self, c, callback=None): # the dict shouldn't be changed while the daemon thread is iterating if self.__running: - debug("Can't unwatch() while running, please use stop()", True) + logger.warning("Can't unwatch() while running, please use stop()") else: - debug("Unwatching command: %s" % str(c)) + logger.info("Unwatching command: %s" % str(c)) if c in self.__commands: # if a callback was specified, only remove the callback @@ -181,9 +184,9 @@ def unwatch_all(self): # the dict shouldn't be changed while the daemon thread is iterating if self.__running: - debug("Can't unwatch_all() while running, please use stop()", True) + logger.warning("Can't unwatch_all() while running, please use stop()") else: - debug("Unwatching all") + logger.info("Unwatching all") self.__commands = {} self.__callbacks = {} diff --git a/obd/commands.py b/obd/commands.py index 1789f3dd..b914fc61 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -32,8 +32,10 @@ from .protocols import ECU from .OBDCommand import OBDCommand from .decoders import * -from .debug import debug +import logging + +logger = logging.getLogger(__name__) @@ -235,7 +237,7 @@ def __getitem__(self, key): elif isinstance(key, str) or isinstance(key, unicode): return self.__dict__[key] else: - debug("OBD commands can only be retrieved by PID value or dict name", True) + logger.warning("OBD commands can only be retrieved by PID value or dict name") def __len__(self): @@ -282,7 +284,7 @@ def set_supported(self, mode, pid, v): if self.has(mode, pid): self.modes[mode][pid].supported = v else: - debug("set_supported() only accepts boolean values", True) + logger.warning("set_supported() only accepts boolean values") def has_command(self, c): @@ -290,7 +292,7 @@ def has_command(self, c): if isinstance(c, OBDCommand): return c in self.__dict__.values() else: - debug("has_command() only accepts OBDCommand objects", True) + logger.warning("has_command() only accepts OBDCommand objects") return False @@ -299,7 +301,7 @@ def has_name(self, s): if isinstance(s, str) or isinstance(s, unicode): return s.isupper() and (s in self.__dict__.keys()) else: - debug("has_name() only accepts string names for commands", True) + logger.warning("has_name() only accepts string names for commands") return False @@ -314,7 +316,7 @@ def has_pid(self, mode, pid): return False return True else: - debug("has_pid() only accepts integer values for mode and PID", True) + logger.warning("has_pid() only accepts integer values for mode and PID") return False diff --git a/obd/decoders.py b/obd/decoders.py index f6767b1a..2df9a25e 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -32,9 +32,13 @@ import math from .utils import * from .codes import * -from .debug import debug from .OBDResponse import Unit, Status, Test +import logging + +logger = logging.getLogger(__name__) + + ''' All decoders take the form: @@ -248,7 +252,7 @@ def elm_voltage(messages): try: return (float(v), Unit.VOLT) except ValueError: - debug("Failed to parse ELM voltage", True) + logger.warning("Failed to parse ELM voltage") return (None, Unit.NONE) @@ -281,7 +285,7 @@ def status(messages): bitToBool(bits[9]))) - # different tests for different ignition types + # different tests for different ignition types if(output.ignition_type == IGNITION_TYPE[0]): # spark for i in range(8): if SPARK_TESTS[i] is not None: @@ -299,7 +303,7 @@ def status(messages): t = Test(COMPRESSION_TESTS[i], \ bitToBool(bits[(2 * 8) + i]), \ bitToBool(bits[(3 * 8) + i])) - + output.tests.append(t) return (output, Unit.NONE) @@ -311,19 +315,19 @@ def fuel_status(messages): v = d[0] # todo, support second fuel system if v <= 0: - debug("Invalid fuel status response (v <= 0)", True) + logger.warning("Invalid fuel status response (v <= 0)") return (None, Unit.NONE) i = math.log(v, 2) # only a single bit should be on if i % 1 != 0: - debug("Invalid fuel status response (multiple bits set)", True) + logger.warning("Invalid fuel status response (multiple bits set)") return (None, Unit.NONE) i = int(i) if i >= len(FUEL_STATUS): - debug("Invalid fuel status response (no table entry)", True) + logger.warning("Invalid fuel status response (no table entry)") return (None, Unit.NONE) return (FUEL_STATUS[i], Unit.NONE) @@ -334,19 +338,19 @@ def air_status(messages): v = d[0] if v <= 0: - debug("Invalid air status response (v <= 0)", True) + logger.warning("Invalid air status response (v <= 0)") return (None, Unit.NONE) i = math.log(v, 2) # only a single bit should be on if i % 1 != 0: - debug("Invalid air status response (multiple bits set)", True) + logger.warning("Invalid air status response (multiple bits set)") return (None, Unit.NONE) i = int(i) if i >= len(AIR_STATUS): - debug("Invalid air status response (no table entry)", True) + logger.warning("Invalid air status response (no table entry)") return (None, Unit.NONE) return (AIR_STATUS[i], Unit.NONE) @@ -361,7 +365,7 @@ def obd_compliance(_hex): if i < len(OBD_COMPLIANCE): v = OBD_COMPLIANCE[i] - return (v, Unit.NONE) + return (v, Unit.NONE) def fuel_type(_hex): diff --git a/obd/obd.py b/obd/obd.py index 3811b7b6..a3004f73 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -37,7 +37,6 @@ from .commands import commands from .OBDResponse import OBDResponse from .utils import scan_serial, OBDStatus -from .debug import debug logger = logging.getLogger(__name__) @@ -183,7 +182,7 @@ def protocol_id(self): def get_port_name(self): # TODO: deprecated, remove later - print("OBD.get_port_name() is deprecated, use OBD.port_name() instead") + logger.warning("OBD.get_port_name() is deprecated, use OBD.port_name() instead") return self.port_name() diff --git a/obd/utils.py b/obd/utils.py index ea3b3cd3..015bc9bb 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -34,7 +34,10 @@ import string import glob import sys -from .debug import debug +import logging + +logger = logging.getLogger(__name__) + class OBDStatus: @@ -161,5 +164,5 @@ def scan_serial(): # TODO: deprecated, remove later def scanSerial(): - print("scanSerial() is deprecated, use scan_serial() instead") + logger.warning("scanSerial() is deprecated, use scan_serial() instead") return scan_serial() From c496b35e87f0e412a8f7832c0bc8465f223b4a1e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 00:35:41 -0400 Subject: [PATCH 352/569] added logging to protocol files --- obd/protocols/protocol.py | 5 ++++- obd/protocols/protocol_can.py | 26 +++++++++++++++----------- obd/protocols/protocol_legacy.py | 20 ++++++++++++-------- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index efe83a9a..01c4b547 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -31,7 +31,10 @@ from binascii import hexlify from obd.utils import isHex, num_bits_set -from obd.debug import debug + +import logging + +logger = logging.getLogger(__name__) """ diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index f02aa35c..d4220ec9 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -31,7 +31,11 @@ from binascii import unhexlify from obd.utils import contiguous -from .protocol import * +from .protocol import Protocol, Message, Frame, ECU + +import logging + +logger = logging.getLogger(__name__) class CANProtocol(Protocol): @@ -66,7 +70,7 @@ def parse_frame(self, frame): # Handle odd size frames and drop if len(raw) & 1: - debug("Dropping frame for being odd") + logger.debug("Dropping frame for being odd") return False raw_bytes = bytearray(unhexlify(raw)) @@ -79,11 +83,11 @@ def parse_frame(self, frame): # # 00 00 07 E8 10 20 ... - debug("Dropped frame for being too short") + logger.debug("Dropped frame for being too short") return False if len(raw_bytes) > 12: - debug("Dropped frame for being too long") + logger.debug("Dropped frame for being too long") return False @@ -127,7 +131,7 @@ def parse_frame(self, frame): if frame.type not in [self.FRAME_TYPE_SF, self.FRAME_TYPE_FF, self.FRAME_TYPE_CF]: - debug("Dropping frame carrying unknown PCI frame type") + logger.debug("Dropping frame carrying unknown PCI frame type") return False @@ -169,7 +173,7 @@ def parse_message(self, message): frame = frames[0] if frame.type != self.FRAME_TYPE_SF: - debug("Recieved lone frame not marked as single frame") + logger.debug("Recieved lone frame not marked as single frame") return False # extract data, ignore PCI byte and anything after the marked length @@ -190,19 +194,19 @@ def parse_message(self, message): elif f.type == self.FRAME_TYPE_CF: cf.append(f) else: - debug("Dropping frame in multi-frame response not marked as FF or CF") + logger.debug("Dropping frame in multi-frame response not marked as FF or CF") # check that we captured only one first-frame if len(ff) > 1: - debug("Recieved multiple frames marked FF") + logger.debug("Recieved multiple frames marked FF") return False elif len(ff) == 0: - debug("Never received frame marked FF") + logger.debug("Never received frame marked FF") return False # check that there was at least one consecutive-frame if len(cf) == 0: - debug("Never received frame marked CF") + logger.debug("Never received frame marked CF") return False # calculate proper sequence indices from the lower 4 bits given @@ -225,7 +229,7 @@ def parse_message(self, message): # check contiguity, and that we aren't missing any frames indices = [f.seq_index for f in cf] if not contiguous(indices, 1, len(cf)): - debug("Recieved multiline response with missing frames") + logger.debug("Recieved multiline response with missing frames") return False diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 22abb137..1e24eeea 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -31,7 +31,11 @@ from binascii import unhexlify from obd.utils import contiguous -from .protocol import * +from .protocol import Protocol, Message, Frame, ECU + +import logging + +logger = logging.getLogger(__name__) class LegacyProtocol(Protocol): @@ -49,17 +53,17 @@ def parse_frame(self, frame): # Handle odd size frames and drop if len(raw) & 1: - debug("Dropping frame for being odd") + logger.debug("Dropping frame for being odd") return False raw_bytes = bytearray(unhexlify(raw)) if len(raw_bytes) < 6: - debug("Dropped frame for being too short") + logger.debug("Dropped frame for being too short") return False if len(raw_bytes) > 11: - debug("Dropped frame for being too long") + logger.debug("Dropped frame for being too long") return False # Ex. @@ -84,15 +88,15 @@ def parse_message(self, message): # len(frames) will always be >= 1 (see the caller, protocol.py) mode = frames[0].data[0] - + # test that all frames are responses to the same Mode (SID) if len(frames) > 1: if not all([mode == f.data[0] for f in frames[1:]]): - debug("Recieved frames from multiple commands") + logger.debug("Recieved frames from multiple commands") return False # legacy protocols have different re-assembly - # procedures for different Modes + # procedures for different Modes if mode == 0x43: # GET_DTC requests return frames with no PID or order bytes @@ -134,7 +138,7 @@ def parse_message(self, message): # check contiguity indices = [f.data[2] for f in frames] if not contiguous(indices, 1, len(frames)): - debug("Recieved multiline response with missing frames") + logger.debug("Recieved multiline response with missing frames") return False # now that they're in order, accumulate the data from each frame From 3e0d3212882abaea36d87b52bc12c41153cd3fa7 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 00:40:41 -0400 Subject: [PATCH 353/569] removed old debug system, wrote docs --- docs/Debug.md | 14 ++++++++------ obd/__init__.py | 2 +- obd/debug.py | 50 ------------------------------------------------- 3 files changed, 9 insertions(+), 57 deletions(-) delete mode 100644 obd/debug.py diff --git a/docs/Debug.md b/docs/Debug.md index 0cacdb7c..ea7fd9dc 100644 --- a/docs/Debug.md +++ b/docs/Debug.md @@ -1,16 +1,18 @@ -python-OBD also contains a debug object that receives status messages and errors. Console printing is disabled by default, but can be enabled manually. A custom debug handler can also be set. +python-OBD uses python's builtin logging system. By default, it is setup to send output to `stderr` with a level of WARNING. The module's logger can be accessed via the `logger` variable at the root of the module. For instance, to enable console printing of all debug messages, use the following snippet: ```python import obd +import logging -obd.debug.console = True +obd.logger.setLevel(logging.DEBUG) +``` -# AND / OR +Or, to silence all logging output from python-OBD: -def log(msg): - print msg +```python +import obd -obd.debug.handler = log +obd.logger.removeHandler(obd.console_handler) ``` --- diff --git a/obd/__init__.py b/obd/__init__.py index 582956c9..1e7851f4 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -48,7 +48,7 @@ import logging logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) +logger.setLevel(logging.WARNING) console_handler = logging.StreamHandler() # sends output to stderr console_handler.setFormatter(logging.Formatter("[%(name)s] %(message)s")) diff --git a/obd/debug.py b/obd/debug.py deleted file mode 100644 index 06105697..00000000 --- a/obd/debug.py +++ /dev/null @@ -1,50 +0,0 @@ - -######################################################################## -# # -# python-OBD: A python OBD-II serial module derived from pyobd # -# # -# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # -# Copyright 2009 Secons Ltd. (www.obdtester.com) # -# Copyright 2009 Peter J. Creath # -# Copyright 2016 Brendan Whitfield (brendan-w.com) # -# # -######################################################################## -# # -# debug.py # -# # -# This file is part of python-OBD (a derivative of pyOBD) # -# # -# python-OBD is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 2 of the License, or # -# (at your option) any later version. # -# # -# python-OBD is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with python-OBD. If not, see . # -# # -######################################################################## - -class Debug(): - def __init__(self): - self.console = False - self.handler = None - - def __call__(self, msg, forcePrint=False): - - if self.console or forcePrint: - print("[obd] " + str(msg)) - - if hasattr(self.handler, '__call__'): - self.handler(msg) - -debug = Debug() - - -class ProtocolError(Exception): - def __init__(self): - pass From 44d03cced3c3ad40f164607e62a400b4d6bca344 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 00:41:33 -0400 Subject: [PATCH 354/569] fixed logging note on troubleshooting page --- docs/Troubleshooting.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index 716d3f3d..317793ac 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -4,7 +4,8 @@ If python-OBD is not working properly, the first thing you should do is enable debug output. Add the following line before your connection code to print all of the debug information to your console: ```python -obd.debug.console = True +import logging +obd.logger.setLevel(logging.DEBUG) ``` Here are some common logs from python-OBD, and their meanings: From c0c4f25e723a7477bf809c18c7cfbc07cacd7db9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 00:44:24 -0400 Subject: [PATCH 355/569] organized command list --- obd/commands.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/obd/commands.py b/obd/commands.py index b914fc61..036e4528 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -66,6 +66,8 @@ OBDCommand("SPEED" , "Vehicle Speed" , b"010D", 1, speed, ECU.ENGINE, True), OBDCommand("TIMING_ADVANCE" , "Timing Advance" , b"010E", 1, timing_advance, ECU.ENGINE, True), OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , b"010F", 1, temp, ECU.ENGINE, True), + + # name description cmd bytes decoder ECU fast OBDCommand("MAF" , "Air Flow Rate (MAF)" , b"0110", 2, maf, ECU.ENGINE, True), OBDCommand("THROTTLE_POS" , "Throttle Position" , b"0111", 1, percent, ECU.ENGINE, True), OBDCommand("AIR_STATUS" , "Secondary Air Status" , b"0112", 1, air_status, ECU.ENGINE, True), @@ -100,6 +102,8 @@ OBDCommand("EGR_ERROR" , "EGR Error" , b"012D", 1, percent_centered, ECU.ENGINE, True), OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , b"012E", 1, percent, ECU.ENGINE, True), OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , b"012F", 1, percent, ECU.ENGINE, True), + + # name description cmd bytes decoder ECU fast OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , b"0130", 1, count, ECU.ENGINE, True), OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , b"0131", 2, distance, ECU.ENGINE, True), OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , b"0132", 2, evap_pressure, ECU.ENGINE, True), @@ -134,6 +138,8 @@ OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , b"014D", 2, minutes, ECU.ENGINE, True), OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , b"014E", 2, minutes, ECU.ENGINE, True), OBDCommand("MAX_VALUES" , "Various Max values" , b"014F", 4, drop, ECU.ENGINE, True), # todo: decode this + + # name description cmd bytes decoder ECU fast OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , b"0150", 4, max_maf, ECU.ENGINE, True), OBDCommand("FUEL_TYPE" , "Fuel Type" , b"0151", 1, fuel_type, ECU.ENGINE, True), OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , b"0152", 1, percent, ECU.ENGINE, True), From 89fb37908e41226ebd5f9d1313f786e6138b1d50 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 15:18:28 -0400 Subject: [PATCH 356/569] confirmed CAN DTC count byte --- obd/protocols/protocol_can.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index d4220ec9..ab1f34f7 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -268,7 +268,9 @@ def parse_message(self, message): mode = message.data[0] if mode == 0x43: - # TODO: confirm this logic. I don't have any raw test data for it yet + # [] + # 43 03 11 11 22 22 33 33 + # [DTC] [DTC] [DTC] # fetch the DTC count, and use it as a length code num_dtc_bytes = message.data[1] * 2 From 4a3b0c2ea7e3a6a06579bab246e3d974a81ec5e2 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 16:25:39 -0400 Subject: [PATCH 357/569] added debug output for the ECU map --- obd/protocols/protocol.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 01c4b547..d0c102ec 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -149,6 +149,11 @@ def __init__(self, lines_0100): # subsequent runs will now be tagged correctly self.populate_ecu_map(messages) + # log out the ecu map + for tx_id, ecu in self.ecu_map.items(): + names = [k for k in ECU.__dict__ if ECU.__dict__[k] == ecu ] + logger.debug("Chose ECU %d as %s" % (tx_id, names)) + def __call__(self, lines): """ From 87901482df4ea17e670a31797b58d637657f4942 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 16:55:11 -0400 Subject: [PATCH 358/569] implemented units using pint --- obd/OBDCommand.py | 2 +- obd/OBDResponse.py | 37 ++++---------------- obd/decoders.py | 86 +++++++++++++++++++++++----------------------- setup.py | 4 +-- 4 files changed, 53 insertions(+), 76 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 967ec9ec..0fc023ab 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -98,7 +98,7 @@ def __call__(self, messages): # and reference to original command r = OBDResponse(self, messages) if messages: - r.value, r.unit = self.decode(messages) + r.value = self.decode(messages) else: logger.info(str(self) + " did not recieve any acceptable messages") diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py index 59148f72..d9f6c600 100644 --- a/obd/OBDResponse.py +++ b/obd/OBDResponse.py @@ -32,33 +32,14 @@ import time +import pint - -class Unit: - """ All unit constants used in python-OBD """ - - NONE = None - RATIO = "Ratio" - COUNT = "Count" - PERCENT = "%" - RPM = "RPM" - VOLT = "Volt" - F = "F" - C = "C" - SEC = "Second" - MIN = "Minute" - PA = "Pa" - KPA = "kPa" - PSI = "psi" - KPH = "kph" - MPH = "mph" - DEGREES = "Degrees" - GPS = "Grams per Second" - MA = "mA" - KM = "km" - LPH = "Liters per Hour" - +# export the unit registry +Unit = pint.UnitRegistry() +Unit.define("percent = [] = %") +Unit.define("gps = gram / second = GPS = grams_per_second") +Unit.define("lph = liter / hour = LPH = liters_per_hour") class OBDResponse(): @@ -68,17 +49,13 @@ def __init__(self, command=None, messages=None): self.command = command self.messages = messages if messages else [] self.value = None - self.unit = Unit.NONE self.time = time.time() def is_null(self): return (not self.messages) or (self.value == None) def __str__(self): - if self.unit != Unit.NONE: - return "%s %s" % (str(self.value), str(self.unit)) - else: - return str(self.value) + return str(self.value) diff --git a/obd/decoders.py b/obd/decoders.py index 2df9a25e..41a9d125 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -51,23 +51,23 @@ def (): # drop all messages, return None def drop(messages): - return (None, Unit.NONE) + return None # data in, data out def noop(messages): - return (messages[0].data, Unit.NONE) + return messages[0].data # hex in, bitstring out def pid(messages): d = messages[0].data v = bytes_to_bits(d) - return (v, Unit.NONE) + return v # returns the raw strings from the ELM def raw_string(messages): - return ("\n".join([m.raw() for m in messages]), Unit.NONE) + return "\n".join([m.raw() for m in messages]) ''' Sensor decoders @@ -77,83 +77,83 @@ def raw_string(messages): def count(messages): d = messages[0].data v = bytes_to_int(d) - return (v, Unit.COUNT) + return v * Unit.count # 0 to 100 % def percent(messages): d = messages[0].data v = d[0] v = v * 100.0 / 255.0 - return (v, Unit.PERCENT) + return v * Unit.percent # -100 to 100 % def percent_centered(messages): d = messages[0].data v = d[0] v = (v - 128) * 100.0 / 128.0 - return (v, Unit.PERCENT) + return v * Unit.percent # -40 to 215 C def temp(messages): d = messages[0].data v = bytes_to_int(d) v = v - 40 - return (v, Unit.C) + return v * Unit.celsius # -40 to 6513.5 C def catalyst_temp(messages): d = messages[0].data v = bytes_to_int(d) v = (v / 10.0) - 40 - return (v, Unit.C) + return v * Unit.celsius # -128 to 128 mA def current_centered(messages): d = messages[0].data v = bytes_to_int(d[2:4]) v = (v / 256.0) - 128 - return (v, Unit.MA) + return v * Unit.milliampere # 0 to 1.275 volts def sensor_voltage(messages): d = messages[0].data v = d[0] v = v / 200.0 - return (v, Unit.VOLT) + return v * Unit.volt # 0 to 8 volts def sensor_voltage_big(messages): d = messages[0].data v = bytes_to_int(d[2:4]) v = (v * 8.0) / 65535 - return (v, Unit.VOLT) + return v * Unit.volt # 0 to 765 kPa def fuel_pressure(messages): d = messages[0].data v = d[0] v = v * 3 - return (v, Unit.KPA) + return v * Unit.kilopascal # 0 to 255 kPa def pressure(messages): d = messages[0].data v = d[0] - return (v, Unit.KPA) + return v * Unit.kilopascal # 0 to 5177 kPa def fuel_pres_vac(messages): d = messages[0].data v = bytes_to_int(d) v = v * 0.079 - return (v, Unit.KPA) + return v * Unit.kilopascal # 0 to 655,350 kPa def fuel_pres_direct(messages): d = messages[0].data v = bytes_to_int(d) v = v * 10 - return (v, Unit.KPA) + return v * Unit.kilopascal # -8192 to 8192 Pa def evap_pressure(messages): @@ -162,86 +162,86 @@ def evap_pressure(messages): a = twos_comp(unhex(d[0]), 8) b = twos_comp(unhex(d[1]), 8) v = ((a * 256.0) + b) / 4.0 - return (v, Unit.PA) + return v * Unit.pascal # 0 to 327.675 kPa def abs_evap_pressure(messages): d = messages[0].data v = bytes_to_int(d) v = v / 200.0 - return (v, Unit.KPA) + return v * Unit.kilopascal # -32767 to 32768 Pa def evap_pressure_alt(messages): d = messages[0].data v = bytes_to_int(d) v = v - 32767 - return (v, Unit.PA) + return v * Unit.pascal # 0 to 16,383.75 RPM def rpm(messages): d = messages[0].data v = bytes_to_int(d) / 4.0 - return (v, Unit.RPM) + return v * Unit.rpm # 0 to 255 KPH def speed(messages): d = messages[0].data v = bytes_to_int(d) - return (v, Unit.KPH) + return v * Unit.kph # -64 to 63.5 degrees def timing_advance(messages): d = messages[0].data v = d[0] v = (v - 128) / 2.0 - return (v, Unit.DEGREES) + return v * Unit.degree # -210 to 301 degrees def inject_timing(messages): d = messages[0].data v = bytes_to_int(d) v = (v - 26880) / 128.0 - return (v, Unit.DEGREES) + return v * Unit.degree # 0 to 655.35 grams/sec def maf(messages): d = messages[0].data v = bytes_to_int(d) v = v / 100.0 - return (v, Unit.GPS) + return v * Unit.gps # 0 to 2550 grams/sec def max_maf(messages): d = messages[0].data v = d[0] v = v * 10 - return (v, Unit.GPS) + return v * Unit.gps # 0 to 65535 seconds def seconds(messages): d = messages[0].data v = bytes_to_int(d) - return (v, Unit.SEC) + return v * Unit.second # 0 to 65535 minutes def minutes(messages): d = messages[0].data v = bytes_to_int(d) - return (v, Unit.MIN) + return v * Unit.minute # 0 to 65535 km def distance(messages): d = messages[0].data v = bytes_to_int(d) - return (v, Unit.KM) + return v * Unit.kilometer # 0 to 3212 Liters/hour def fuel_rate(messages): d = messages[0].data v = bytes_to_int(d) v = v * 0.05 - return (v, Unit.LPH) + return v * Unit.liters_per_hour def elm_voltage(messages): @@ -250,10 +250,10 @@ def elm_voltage(messages): v = messages[0].frames[0].raw try: - return (float(v), Unit.VOLT) + return float(v) * Unit.volt except ValueError: logger.warning("Failed to parse ELM voltage") - return (None, Unit.NONE) + return None ''' @@ -306,7 +306,7 @@ def status(messages): output.tests.append(t) - return (output, Unit.NONE) + return output @@ -316,21 +316,21 @@ def fuel_status(messages): if v <= 0: logger.warning("Invalid fuel status response (v <= 0)") - return (None, Unit.NONE) + return None i = math.log(v, 2) # only a single bit should be on if i % 1 != 0: logger.warning("Invalid fuel status response (multiple bits set)") - return (None, Unit.NONE) + return None i = int(i) if i >= len(FUEL_STATUS): logger.warning("Invalid fuel status response (no table entry)") - return (None, Unit.NONE) + return None - return (FUEL_STATUS[i], Unit.NONE) + return FUEL_STATUS[i] def air_status(messages): @@ -339,21 +339,21 @@ def air_status(messages): if v <= 0: logger.warning("Invalid air status response (v <= 0)") - return (None, Unit.NONE) + return None i = math.log(v, 2) # only a single bit should be on if i % 1 != 0: logger.warning("Invalid air status response (multiple bits set)") - return (None, Unit.NONE) + return None i = int(i) if i >= len(AIR_STATUS): logger.warning("Invalid air status response (no table entry)") - return (None, Unit.NONE) + return None - return (AIR_STATUS[i], Unit.NONE) + return AIR_STATUS[i] def obd_compliance(_hex): @@ -365,7 +365,7 @@ def obd_compliance(_hex): if i < len(OBD_COMPLIANCE): v = OBD_COMPLIANCE[i] - return (v, Unit.NONE) + return v def fuel_type(_hex): @@ -377,7 +377,7 @@ def fuel_type(_hex): if i < len(FUEL_TYPES): v = FUEL_TYPES[i] - return (v, Unit.NONE) + return v def single_dtc(_bytes): @@ -424,4 +424,4 @@ def dtc(messages): codes.append( (dtc, desc) ) - return (codes, Unit.NONE) + return codes diff --git a/setup.py b/setup.py index 3b79abdc..21bd121b 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="obd", - version="0.5.0", + version="0.6.0", description=("Serial module for handling live sensor data from a vehicle's OBD-II port"), classifiers=[ "Operating System :: POSIX :: Linux", @@ -25,5 +25,5 @@ packages=find_packages(), include_package_data=True, zip_safe=False, - install_requires=["pyserial"], + install_requires=["pyserial", "pint"], ) From 1d03abf99734233a4f2afe9b65c51a4c2db416e0 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 17:27:31 -0400 Subject: [PATCH 359/569] fixed tests for new Pint units --- obd/decoders.py | 4 +- tests/test_decoders.py | 183 +++++++++++++++++++++-------------------- 2 files changed, 95 insertions(+), 92 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 41a9d125..7f8baa2e 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -98,14 +98,14 @@ def temp(messages): d = messages[0].data v = bytes_to_int(d) v = v - 40 - return v * Unit.celsius + return Unit.Quantity(v, Unit.celsius) # non-multiplicative unit # -40 to 6513.5 C def catalyst_temp(messages): d = messages[0].data v = bytes_to_int(d) v = (v / 10.0) - 40 - return v * Unit.celsius + return Unit.Quantity(v, Unit.celsius) # non-multiplicative unit # -128 to 128 mA def current_centered(messages): diff --git a/tests/test_decoders.py b/tests/test_decoders.py index cd6b783f..dffed7cf 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -15,9 +15,12 @@ def m(hex_data, frames=[]): return [message] -def float_equals(d1, d2): - values_match = (abs(d1[0] - d2[0]) < 0.02) - units_match = (d1[1] == d2[1]) +FLOAT_EQUALS_TOLERANCE = 0.02 + +# comparison for pint floating point values +def float_equals(va, vb): + units_match = (va.u == vb.u) + values_match = (abs(va.magnitude - vb.magnitude) < FLOAT_EQUALS_TOLERANCE) return values_match and units_match @@ -25,173 +28,173 @@ def float_equals(d1, d2): def test_noop(): - assert d.noop(m("00010203")) == (bytearray([0, 1, 2, 3]), Unit.NONE) + assert d.noop(m("00010203")) == bytearray([0, 1, 2, 3]) def test_drop(): - assert d.drop(m("deadbeef")) == (None, Unit.NONE) + assert d.drop(m("deadbeef")) == None def test_raw_string(): - assert d.raw_string([ Message([]) ]) == ("", Unit.NONE) - assert d.raw_string([ Message([ Frame("NO DATA") ]) ]) == ("NO DATA", Unit.NONE) - assert d.raw_string([ Message([ Frame("A"), Frame("B") ]) ]) == ("A\nB", Unit.NONE) - assert d.raw_string([ Message([ Frame("A") ]), Message([ Frame("B") ]) ]) == ("A\nB", Unit.NONE) + assert d.raw_string([ Message([]) ]) == "" + assert d.raw_string([ Message([ Frame("NO DATA") ]) ]) == "NO DATA" + assert d.raw_string([ Message([ Frame("A"), Frame("B") ]) ]) == "A\nB" + assert d.raw_string([ Message([ Frame("A") ]), Message([ Frame("B") ]) ]) == "A\nB" def test_pid(): - assert d.pid(m("00000000")) == ("00000000000000000000000000000000", Unit.NONE) - assert d.pid(m("F00AA00F")) == ("11110000000010101010000000001111", Unit.NONE) - assert d.pid(m("11")) == ("00010001", Unit.NONE) + assert d.pid(m("00000000")) == "00000000000000000000000000000000" + assert d.pid(m("F00AA00F")) == "11110000000010101010000000001111" + assert d.pid(m("11")) == "00010001" def test_count(): - assert d.count(m("00")) == (0, Unit.COUNT) - assert d.count(m("0F")) == (15, Unit.COUNT) - assert d.count(m("03E8")) == (1000, Unit.COUNT) + assert d.count(m("00")) == 0 * Unit.count + assert d.count(m("0F")) == 15 * Unit.count + assert d.count(m("03E8")) == 1000 * Unit.count def test_percent(): - assert d.percent(m("00")) == (0.0, Unit.PERCENT) - assert d.percent(m("FF")) == (100.0, Unit.PERCENT) + assert d.percent(m("00")) == 0.0 * Unit.percent + assert d.percent(m("FF")) == 100.0 * Unit.percent def test_percent_centered(): - assert d.percent_centered(m("00")) == (-100.0, Unit.PERCENT) - assert d.percent_centered(m("80")) == (0.0, Unit.PERCENT) - assert float_equals(d.percent_centered(m("FF")), (99.2, Unit.PERCENT)) + assert d.percent_centered(m("00")) == -100.0 * Unit.percent + assert d.percent_centered(m("80")) == 0.0 * Unit.percent + assert float_equals(d.percent_centered(m("FF")), 99.2 * Unit.percent) def test_temp(): - assert d.temp(m("00")) == (-40, Unit.C) - assert d.temp(m("FF")) == (215, Unit.C) - assert d.temp(m("03E8")) == (960, Unit.C) + assert d.temp(m("00")) == Unit.Quantity(-40, Unit.celsius) + assert d.temp(m("FF")) == Unit.Quantity(215, Unit.celsius) + assert d.temp(m("03E8")) == Unit.Quantity(960, Unit.celsius) def test_catalyst_temp(): - assert d.catalyst_temp(m("0000")) == (-40.0, Unit.C) - assert d.catalyst_temp(m("FFFF")) == (6513.5, Unit.C) + assert d.catalyst_temp(m("0000")) == Unit.Quantity(-40.0, Unit.celsius) + assert d.catalyst_temp(m("FFFF")) == Unit.Quantity(6513.5, Unit.celsius) def test_current_centered(): - assert d.current_centered(m("00000000")) == (-128.0, Unit.MA) - assert d.current_centered(m("00008000")) == (0.0, Unit.MA) - assert float_equals(d.current_centered(m("0000FFFF")), (128.0, Unit.MA)) - assert d.current_centered(m("ABCD8000")) == (0.0, Unit.MA) # first 2 bytes are unused (should be disregarded) + assert d.current_centered(m("00000000")) == -128.0 * Unit.milliampere + assert d.current_centered(m("00008000")) == 0.0 * Unit.milliampere + assert d.current_centered(m("ABCD8000")) == 0.0 * Unit.milliampere # first 2 bytes are unused (should be disregarded) + assert float_equals(d.current_centered(m("0000FFFF")), 128.0 * Unit.milliampere) def test_sensor_voltage(): - assert d.sensor_voltage(m("0000")) == (0.0, Unit.VOLT) - assert d.sensor_voltage(m("FFFF")) == (1.275, Unit.VOLT) + assert d.sensor_voltage(m("0000")) == 0.0 * Unit.volt + assert d.sensor_voltage(m("FFFF")) == 1.275 * Unit.volt def test_sensor_voltage_big(): - assert d.sensor_voltage_big(m("00000000")) == (0.0, Unit.VOLT) - assert float_equals(d.sensor_voltage_big(m("00008000")), (4.0, Unit.VOLT)) - assert d.sensor_voltage_big(m("0000FFFF")) == (8.0, Unit.VOLT) - assert d.sensor_voltage_big(m("ABCD0000")) == (0.0, Unit.VOLT) # first 2 bytes are unused (should be disregarded) + assert d.sensor_voltage_big(m("00000000")) == 0.0 * Unit.volt + assert float_equals(d.sensor_voltage_big(m("00008000")), 4.0 * Unit.volt) + assert d.sensor_voltage_big(m("0000FFFF")) == 8.0 * Unit.volt + assert d.sensor_voltage_big(m("ABCD0000")) == 0.0 * Unit.volt # first 2 bytes are unused (should be disregarded) def test_fuel_pressure(): - assert d.fuel_pressure(m("00")) == (0, Unit.KPA) - assert d.fuel_pressure(m("80")) == (384, Unit.KPA) - assert d.fuel_pressure(m("FF")) == (765, Unit.KPA) + assert d.fuel_pressure(m("00")) == 0 * Unit.kilopascal + assert d.fuel_pressure(m("80")) == 384 * Unit.kilopascal + assert d.fuel_pressure(m("FF")) == 765 * Unit.kilopascal def test_pressure(): - assert d.pressure(m("00")) == (0, Unit.KPA) - assert d.pressure(m("00")) == (0, Unit.KPA) + assert d.pressure(m("00")) == 0 * Unit.kilopascal + assert d.pressure(m("00")) == 0 * Unit.kilopascal def test_fuel_pres_vac(): - assert d.fuel_pres_vac(m("0000")) == (0.0, Unit.KPA) - assert d.fuel_pres_vac(m("FFFF")) == (5177.265, Unit.KPA) + assert d.fuel_pres_vac(m("0000")) == 0.0 * Unit.kilopascal + assert d.fuel_pres_vac(m("FFFF")) == 5177.265 * Unit.kilopascal def test_fuel_pres_direct(): - assert d.fuel_pres_direct(m("0000")) == (0, Unit.KPA) - assert d.fuel_pres_direct(m("FFFF")) == (655350, Unit.KPA) + assert d.fuel_pres_direct(m("0000")) == 0 * Unit.kilopascal + assert d.fuel_pres_direct(m("FFFF")) == 655350 * Unit.kilopascal def test_evap_pressure(): pass # TODO - #assert d.evap_pressure(m("0000")) == (0.0, Unit.PA) + #assert d.evap_pressure(m("0000")) == 0.0 * Unit.PA) def test_abs_evap_pressure(): - assert d.abs_evap_pressure(m("0000")) == (0, Unit.KPA) - assert d.abs_evap_pressure(m("FFFF")) == (327.675, Unit.KPA) + assert d.abs_evap_pressure(m("0000")) == 0 * Unit.kilopascal + assert d.abs_evap_pressure(m("FFFF")) == 327.675 * Unit.kilopascal def test_evap_pressure_alt(): - assert d.evap_pressure_alt(m("0000")) == (-32767, Unit.PA) - assert d.evap_pressure_alt(m("7FFF")) == (0, Unit.PA) - assert d.evap_pressure_alt(m("FFFF")) == (32768, Unit.PA) + assert d.evap_pressure_alt(m("0000")) == -32767 * Unit.pascal + assert d.evap_pressure_alt(m("7FFF")) == 0 * Unit.pascal + assert d.evap_pressure_alt(m("FFFF")) == 32768 * Unit.pascal def test_rpm(): - assert d.rpm(m("0000")) == (0.0, Unit.RPM) - assert d.rpm(m("FFFF")) == (16383.75, Unit.RPM) + assert d.rpm(m("0000")) == 0.0 * Unit.rpm + assert d.rpm(m("FFFF")) == 16383.75 * Unit.rpm def test_speed(): - assert d.speed(m("00")) == (0, Unit.KPH) - assert d.speed(m("FF")) == (255, Unit.KPH) + assert d.speed(m("00")) == 0 * Unit.kph + assert d.speed(m("FF")) == 255 * Unit.kph def test_timing_advance(): - assert d.timing_advance(m("00")) == (-64.0, Unit.DEGREES) - assert d.timing_advance(m("FF")) == (63.5, Unit.DEGREES) + assert d.timing_advance(m("00")) == -64.0 * Unit.degrees + assert d.timing_advance(m("FF")) == 63.5 * Unit.degrees def test_inject_timing(): - assert d.inject_timing(m("0000")) == (-210, Unit.DEGREES) - assert float_equals(d.inject_timing(m("FFFF")), (302, Unit.DEGREES)) + assert d.inject_timing(m("0000")) == -210 * Unit.degrees + assert float_equals(d.inject_timing(m("FFFF")), 302 * Unit.degrees) def test_maf(): - assert d.maf(m("0000")) == (0.0, Unit.GPS) - assert d.maf(m("FFFF")) == (655.35, Unit.GPS) + assert d.maf(m("0000")) == 0.0 * Unit.grams_per_second + assert d.maf(m("FFFF")) == 655.35 * Unit.grams_per_second def test_max_maf(): - assert d.max_maf(m("00000000")) == (0, Unit.GPS) - assert d.max_maf(m("FF000000")) == (2550, Unit.GPS) - assert d.max_maf(m("00ABCDEF")) == (0, Unit.GPS) # last 3 bytes are unused (should be disregarded) + assert d.max_maf(m("00000000")) == 0 * Unit.grams_per_second + assert d.max_maf(m("FF000000")) == 2550 * Unit.grams_per_second + assert d.max_maf(m("00ABCDEF")) == 0 * Unit.grams_per_second # last 3 bytes are unused (should be disregarded) def test_seconds(): - assert d.seconds(m("0000")) == (0, Unit.SEC) - assert d.seconds(m("FFFF")) == (65535, Unit.SEC) + assert d.seconds(m("0000")) == 0 * Unit.second + assert d.seconds(m("FFFF")) == 65535 * Unit.second def test_minutes(): - assert d.minutes(m("0000")) == (0, Unit.MIN) - assert d.minutes(m("FFFF")) == (65535, Unit.MIN) + assert d.minutes(m("0000")) == 0 * Unit.minute + assert d.minutes(m("FFFF")) == 65535 * Unit.minute def test_distance(): - assert d.distance(m("0000")) == (0, Unit.KM) - assert d.distance(m("FFFF")) == (65535, Unit.KM) + assert d.distance(m("0000")) == 0 * Unit.kilometer + assert d.distance(m("FFFF")) == 65535 * Unit.kilometer def test_fuel_rate(): - assert d.fuel_rate(m("0000")) == (0.0, Unit.LPH) - assert d.fuel_rate(m("FFFF")) == (3276.75, Unit.LPH) + assert d.fuel_rate(m("0000")) == 0.0 * Unit.liters_per_hour + assert d.fuel_rate(m("FFFF")) == 3276.75 * Unit.liters_per_hour def test_fuel_status(): - assert d.fuel_status(m("0100")) == ("Open loop due to insufficient engine temperature", Unit.NONE) - assert d.fuel_status(m("0800")) == ("Open loop due to system failure", Unit.NONE) - assert d.fuel_status(m("0300")) == (None, Unit.NONE) + assert d.fuel_status(m("0100")) == "Open loop due to insufficient engine temperature" + assert d.fuel_status(m("0800")) == "Open loop due to system failure" + assert d.fuel_status(m("0300")) == None def test_air_status(): - assert d.air_status(m("01")) == ("Upstream", Unit.NONE) - assert d.air_status(m("08")) == ("Pump commanded on for diagnostics", Unit.NONE) - assert d.air_status(m("03")) == (None, Unit.NONE) + assert d.air_status(m("01")) == "Upstream" + assert d.air_status(m("08")) == "Pump commanded on for diagnostics" + assert d.air_status(m("03")) == None def test_elm_voltage(): # these aren't parsed as standard hex messages, so manufacture our own - assert d.elm_voltage([ Message([ Frame("12.875") ]) ]) == (12.875, Unit.VOLT) - assert d.elm_voltage([ Message([ Frame("12") ]) ]) == (12, Unit.VOLT) - assert d.elm_voltage([ Message([ Frame("12ABCD") ]) ]) == (None, Unit.NONE) + assert d.elm_voltage([ Message([ Frame("12.875") ]) ]) == 12.875 * Unit.volt + assert d.elm_voltage([ Message([ Frame("12") ]) ]) == 12 * Unit.volt + assert d.elm_voltage([ Message([ Frame("12ABCD") ]) ]) == None def test_dtc(): - assert d.dtc(m("0104")) == ([ + assert d.dtc(m("0104")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), - ], Unit.NONE) + ] # multiple codes - assert d.dtc(m("010480034123")) == ([ + assert d.dtc(m("010480034123")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ("B0003", "Unknown error code"), ("C0123", "Unknown error code"), - ], Unit.NONE) + ] # invalid code lengths are dropped - assert d.dtc(m("0104800341")) == ([ + assert d.dtc(m("0104800341")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ("B0003", "Unknown error code"), - ], Unit.NONE) + ] # 0000 codes are dropped - assert d.dtc(m("000001040000")) == ([ + assert d.dtc(m("000001040000")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), - ], Unit.NONE) + ] # test multiple messages - assert d.dtc(m("0104") + m("8003") + m("0000")) == ([ + assert d.dtc(m("0104") + m("8003") + m("0000")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ("B0003", "Unknown error code"), - ], Unit.NONE) + ] From 125ea38f918aaa0f8036d1740c50c42bf8ada85b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 17:30:19 -0400 Subject: [PATCH 360/569] fixed obdsim test for change to Pint quantities --- tests/test_obdsim.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_obdsim.py b/tests/test_obdsim.py index 287ade6c..abf4c429 100644 --- a/tests/test_obdsim.py +++ b/tests/test_obdsim.py @@ -35,9 +35,7 @@ def async(request): def good_rpm_response(r): - return isinstance(r.value, float) and \ - r.value >= 0.0 and \ - r.unit == Unit.RPM + return (r.value.u == Unit.rpm) and (r.value >= 0.0 * Unit.rpm) def test_supports(obd): assert(len(obd.supported_commands) > 0) From edb55baf6bcc6b5fe699f009f43dfaacc650897f Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 17:43:16 -0400 Subject: [PATCH 361/569] added a new unit property for backwards compatibility --- obd/OBDResponse.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py index d9f6c600..073dbb91 100644 --- a/obd/OBDResponse.py +++ b/obd/OBDResponse.py @@ -51,6 +51,16 @@ def __init__(self, command=None, messages=None): self.value = None self.time = time.time() + @property + def unit(self): + # for backwards compatibility + if isinstance(self.value, Unit.Quantity): + return str(self.value.u) + elif self.value == None: + return None + else: + return str(type(self.value)) + def is_null(self): return (not self.messages) or (self.value == None) From 171c0a67247f08428518445b0ecf0643bb8e3463 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 19:51:32 -0400 Subject: [PATCH 362/569] documented pint units --- README.md | 1 - docs/Responses.md | 67 +++++++++++++++++++++++++++-------------------- 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 3bc11a5a..c6fe81a8 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,6 @@ cmd = obd.commands.RPM # select an OBD command (sensor) response = connection.query(cmd) # send the command, and parse the response print(response.value) -print(response.unit) ``` Documentation diff --git a/docs/Responses.md b/docs/Responses.md index fe72b3b6..1cb292cd 100644 --- a/docs/Responses.md +++ b/docs/Responses.md @@ -3,12 +3,10 @@ The `query()` function returns `OBDResponse` objects. These objects have the fol | Property | Description | |----------|------------------------------------------------------------------------| | value | The decoded value from the car | -| unit | The units of the decoded value | -| command | The `OBDCommand` object that triggered this response | +| command | The `OBDCommand` object that triggered this response | | message | The internal `Message` object containing the raw response from the car | | time | Timestamp of response (as given by [`time.time()`](https://docs.python.org/2/library/time.html#time.time)) | -The `value` property typically contains numeric values, but can also hold complex structures (depending upon the command that was sent). --- @@ -27,36 +25,47 @@ if not r.is_null(): --- -# Units +# Values -Unit values can be found in the `Unit` class (enum). +The `value` property typically contains a [Pint](http://pint.readthedocs.io/en/latest/) `Quantity` object, but can also hold complex structures (depending on the request). Pint quantities combine a value and unit into a single class, and are used to represent physical values (such as "4 seconds", and "88 mph"). This allows for consistency when doing math and unit conversions. Pint maintains a registry of units, which is exposed in python-OBD as `obd.Unit`. + +Below are common operations that can be done with Pint units and quantities. For more information, check out the [Pint Documentation](http://pint.readthedocs.io/en/latest/). ```python -from obd.utils import Unit -``` +import obd + +>>> response.value + + +# get the raw python datatype +>>> response.value.magnitude +100 + +# converts quantities to strings +>>> str(response.value) +'100 kph' -| Name | Value | -|-------------|--------------------| -| NONE | None | -| RATIO | "Ratio" | -| COUNT | "Count" | -| PERCENT | "%" | -| RPM | "RPM" | -| VOLT | "Volt" | -| F | "F" | -| C | "C" | -| SEC | "Second" | -| MIN | "Minute" | -| PA | "Pa" | -| KPA | "kPa" | -| PSI | "psi" | -| KPH | "kph" | -| MPH | "mph" | -| DEGREES | "Degrees" | -| GPS | "Grams per Second" | -| MA | "mA" | -| KM | "km" | -| LPH | "Liters per Hour" | +# convert strings to quantities +>>> obd.Unit("100 kph") + + +# handles conversions nicely +>>> response.value.to('mph') + + +# scaler math +>>> response.value / 2 + + +# non-scaler math requires you to specify units yourself +>>> response.value + (20 * obd.Unit.kph) + + +# non-scaler math with different units +# handles unit conversions transparently +>>> response.value + (20 * obd.Unit.mph) + +``` --- From 0611a71a4cbe88426f8d15556b3b68aeccdd4585 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 22:15:40 -0400 Subject: [PATCH 363/569] wrote up the mode 06 table with drop decoder --- obd/commands.py | 112 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 3 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 036e4528..698f4078 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -179,6 +179,108 @@ OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , b"04", 0, drop, ECU.ALL, False), ] +__mode6__ = [ + # name description cmd bytes decoder ECU fast + OBDCommand("MIDS_A" , "Supported MIDs [01-20]" , b"0600", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_B1S1" , "O2 Sensor Monitor Bank 1 - Sensor 1" , b"0601", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_B1S2" , "O2 Sensor Monitor Bank 1 - Sensor 2" , b"0602", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_B1S3" , "O2 Sensor Monitor Bank 1 - Sensor 3" , b"0603", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_B1S4" , "O2 Sensor Monitor Bank 1 - Sensor 4" , b"0604", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_B2S1" , "O2 Sensor Monitor Bank 2 - Sensor 1" , b"0605", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_B2S2" , "O2 Sensor Monitor Bank 2 - Sensor 2" , b"0606", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_B2S3" , "O2 Sensor Monitor Bank 2 - Sensor 3" , b"0607", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_B2S4" , "O2 Sensor Monitor Bank 2 - Sensor 4" , b"0608", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_B3S1" , "O2 Sensor Monitor Bank 3 - Sensor 1" , b"0609", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_B3S2" , "O2 Sensor Monitor Bank 3 - Sensor 2" , b"060A", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_B3S3" , "O2 Sensor Monitor Bank 3 - Sensor 3" , b"060B", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_B3S4" , "O2 Sensor Monitor Bank 3 - Sensor 4" , b"060C", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_B4S1" , "O2 Sensor Monitor Bank 4 - Sensor 1" , b"060D", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_B4S2" , "O2 Sensor Monitor Bank 4 - Sensor 2" , b"060E", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_B4S3" , "O2 Sensor Monitor Bank 4 - Sensor 3" , b"060F", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_B4S4" , "O2 Sensor Monitor Bank 4 - Sensor 4" , b"0610", 0, drop, ECU.ALL, False), +] + ([None] * 15) + [ # 11 - 1F Reserved + OBDCommand("MIDS_B" , "Supported MIDs [21-40]" , b"0620", 0, drop, ECU.ALL, False), + OBDCommand("MON_CATALYST_B1" , "Catalyst Monitor Bank 1" , b"0621", 0, drop, ECU.ALL, False), + OBDCommand("MON_CATALYST_B2" , "Catalyst Monitor Bank 2" , b"0622", 0, drop, ECU.ALL, False), + OBDCommand("MON_CATALYST_B3" , "Catalyst Monitor Bank 3" , b"0623", 0, drop, ECU.ALL, False), + OBDCommand("MON_CATALYST_B4" , "Catalyst Monitor Bank 4" , b"0624", 0, drop, ECU.ALL, False), +] + ([None] * 12) + [ # 25 - 30 Reserved + OBDCommand("MON_EGR_B1" , "EGR Monitor Bank 1" , b"0631", 0, drop, ECU.ALL, False), + OBDCommand("MON_EGR_B2" , "EGR Monitor Bank 2" , b"0632", 0, drop, ECU.ALL, False), + OBDCommand("MON_EGR_B3" , "EGR Monitor Bank 3" , b"0633", 0, drop, ECU.ALL, False), + OBDCommand("MON_EGR_B4" , "EGR Monitor Bank 4" , b"0634", 0, drop, ECU.ALL, False), + OBDCommand("MON_VVT_B1" , "VVT Monitor Bank 1" , b"0635", 0, drop, ECU.ALL, False), + OBDCommand("MON_VVT_B2" , "VVT Monitor Bank 2" , b"0636", 0, drop, ECU.ALL, False), + OBDCommand("MON_VVT_B3" , "VVT Monitor Bank 3" , b"0637", 0, drop, ECU.ALL, False), + OBDCommand("MON_VVT_B4" , "VVT Monitor Bank 4" , b"0638", 0, drop, ECU.ALL, False), + OBDCommand("MON_EVAP_150" , "EVAP Monitor (Cap Off / 0.150\")" , b"0639", 0, drop, ECU.ALL, False), + OBDCommand("MON_EVAP_090" , "EVAP Monitor (0.090\")" , b"063A", 0, drop, ECU.ALL, False), + OBDCommand("MON_EVAP_040" , "EVAP Monitor (0.040\")" , b"063B", 0, drop, ECU.ALL, False), + OBDCommand("MON_EVAP_020" , "EVAP Monitor (0.020\")" , b"063C", 0, drop, ECU.ALL, False), + OBDCommand("MON_PURGE_FLOW" , "Purge Flow Monitor" , b"063D", 0, drop, ECU.ALL, False), +] + ([None] * 2) + [ # 3E - 3F Reserved + OBDCommand("MIDS_C" , "Supported MIDs [41-60]" , b"0640", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_HEATER_B1S1" , "O2 Sensor Heater Monitor Bank 1 - Sensor 1" , b"0641", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_HEATER_B1S2" , "O2 Sensor Heater Monitor Bank 1 - Sensor 2" , b"0642", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_HEATER_B1S3" , "O2 Sensor Heater Monitor Bank 1 - Sensor 3" , b"0643", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_HEATER_B1S4" , "O2 Sensor Heater Monitor Bank 1 - Sensor 4" , b"0644", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_HEATER_B2S1" , "O2 Sensor Heater Monitor Bank 2 - Sensor 1" , b"0645", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_HEATER_B2S2" , "O2 Sensor Heater Monitor Bank 2 - Sensor 2" , b"0646", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_HEATER_B2S3" , "O2 Sensor Heater Monitor Bank 2 - Sensor 3" , b"0647", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_HEATER_B2S4" , "O2 Sensor Heater Monitor Bank 2 - Sensor 4" , b"0648", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_HEATER_B3S1" , "O2 Sensor Heater Monitor Bank 3 - Sensor 1" , b"0649", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_HEATER_B3S2" , "O2 Sensor Heater Monitor Bank 3 - Sensor 2" , b"064A", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_HEATER_B3S3" , "O2 Sensor Heater Monitor Bank 3 - Sensor 3" , b"064B", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_HEATER_B3S4" , "O2 Sensor Heater Monitor Bank 3 - Sensor 4" , b"064C", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_HEATER_B4S1" , "O2 Sensor Heater Monitor Bank 4 - Sensor 1" , b"064D", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_HEATER_B4S2" , "O2 Sensor Heater Monitor Bank 4 - Sensor 2" , b"064E", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_HEATER_B4S3" , "O2 Sensor Heater Monitor Bank 4 - Sensor 3" , b"064F", 0, drop, ECU.ALL, False), + OBDCommand("MON_O2_HEATER_B4S4" , "O2 Sensor Heater Monitor Bank 4 - Sensor 4" , b"0650", 0, drop, ECU.ALL, False), +] + ([None] * 15) + [ # 51 - 5F Reserved + OBDCommand("MIDS_D" , "Supported MIDs [61-80]" , b"0660", 0, drop, ECU.ALL, False), + OBDCommand("MON_HEATED_CATALYST_B1" , "Heated Catalyst Monitor Bank 1" , b"0661", 0, drop, ECU.ALL, False), + OBDCommand("MON_HEATED_CATALYST_B2" , "Heated Catalyst Monitor Bank 2" , b"0662", 0, drop, ECU.ALL, False), + OBDCommand("MON_HEATED_CATALYST_B3" , "Heated Catalyst Monitor Bank 3" , b"0663", 0, drop, ECU.ALL, False), + OBDCommand("MON_HEATED_CATALYST_B4" , "Heated Catalyst Monitor Bank 4" , b"0664", 0, drop, ECU.ALL, False), +] + ([None] * 12) + [ # 65 - 70 Reserved + OBDCommand("MON_SECONDARY_AIR_1" , "Secondary Air Monitor 1" , b"0671", 0, drop, ECU.ALL, False), + OBDCommand("MON_SECONDARY_AIR_2" , "Secondary Air Monitor 2" , b"0672", 0, drop, ECU.ALL, False), + OBDCommand("MON_SECONDARY_AIR_3" , "Secondary Air Monitor 3" , b"0673", 0, drop, ECU.ALL, False), + OBDCommand("MON_SECONDARY_AIR_4" , "Secondary Air Monitor 4" , b"0674", 0, drop, ECU.ALL, False), +] + ([None] * 11) + [ # 75 - 7F Reserved + OBDCommand("MIDS_E" , "Supported MIDs [81-A0]" , b"0680", 0, drop, ECU.ALL, False), + OBDCommand("MON_FUEL_SYSTEM_B1" , "Fuel System Monitor Bank 1" , b"0681", 0, drop, ECU.ALL, False), + OBDCommand("MON_FUEL_SYSTEM_B2" , "Fuel System Monitor Bank 2" , b"0682", 0, drop, ECU.ALL, False), + OBDCommand("MON_FUEL_SYSTEM_B3" , "Fuel System Monitor Bank 3" , b"0683", 0, drop, ECU.ALL, False), + OBDCommand("MON_FUEL_SYSTEM_B4" , "Fuel System Monitor Bank 4" , b"0684", 0, drop, ECU.ALL, False), + OBDCommand("MON_BOOST_PRESSURE_B1" , "Boost Pressure Control Monitor Bank 1" , b"0685", 0, drop, ECU.ALL, False), + OBDCommand("MON_BOOST_PRESSURE_B2" , "Boost Pressure Control Monitor Bank 1" , b"0686", 0, drop, ECU.ALL, False), +] + ([None] * 9) + [ # 87 - 8F Reserved + OBDCommand("MON_NOX_ABSORBER_B1" , "NOx Absorber Monitor Bank 1" , b"0690", 0, drop, ECU.ALL, False), + OBDCommand("MON_NOX_ABSORBER_B2" , "NOx Absorber Monitor Bank 2" , b"0691", 0, drop, ECU.ALL, False), +] + ([None] * 6) + [ # 92 - 97 Reserved + OBDCommand("MON_NOX_CATALYST_B1" , "NOx Catalyst Monitor Bank 1" , b"0698", 0, drop, ECU.ALL, False), + OBDCommand("MON_NOX_CATALYST_B2" , "NOx Catalyst Monitor Bank 2" , b"0699", 0, drop, ECU.ALL, False), +] + ([None] * 6) + [ # 9A - 9F Reserved + OBDCommand("MIDS_F" , "Supported MIDs [A1-C0]" , b"06A0", 0, drop, ECU.ALL, False), + OBDCommand("MON_MISFIRE_GENERAL" , "Misfire Monitor General Data" , b"06A1", 0, drop, ECU.ALL, False), + OBDCommand("MON_MISFIRE_CYLINDER_1" , "Misfire Cylinder 1 Data" , b"06A2", 0, drop, ECU.ALL, False), + OBDCommand("MON_MISFIRE_CYLINDER_2" , "Misfire Cylinder 2 Data" , b"06A3", 0, drop, ECU.ALL, False), + OBDCommand("MON_MISFIRE_CYLINDER_3" , "Misfire Cylinder 3 Data" , b"06A4", 0, drop, ECU.ALL, False), + OBDCommand("MON_MISFIRE_CYLINDER_4" , "Misfire Cylinder 4 Data" , b"06A5", 0, drop, ECU.ALL, False), + OBDCommand("MON_MISFIRE_CYLINDER_5" , "Misfire Cylinder 5 Data" , b"06A6", 0, drop, ECU.ALL, False), + OBDCommand("MON_MISFIRE_CYLINDER_6" , "Misfire Cylinder 6 Data" , b"06A7", 0, drop, ECU.ALL, False), + OBDCommand("MON_MISFIRE_CYLINDER_7" , "Misfire Cylinder 7 Data" , b"06A8", 0, drop, ECU.ALL, False), + OBDCommand("MON_MISFIRE_CYLINDER_8" , "Misfire Cylinder 8 Data" , b"06A9", 0, drop, ECU.ALL, False), + OBDCommand("MON_MISFIRE_CYLINDER_9" , "Misfire Cylinder 9 Data" , b"06AA", 0, drop, ECU.ALL, False), + OBDCommand("MON_MISFIRE_CYLINDER_10" , "Misfire Cylinder 10 Data" , b"06AB", 0, drop, ECU.ALL, False), + OBDCommand("MON_MISFIRE_CYLINDER_11" , "Misfire Cylinder 11 Data" , b"06AC", 0, drop, ECU.ALL, False), + OBDCommand("MON_MISFIRE_CYLINDER_12" , "Misfire Cylinder 12 Data" , b"06AD", 0, drop, ECU.ALL, False), +] + ([None] * 2) + [ # AE - AF Reserved + OBDCommand("MON_PM_FILTER_B1" , "PM Filter Monitor Bank 1" , b"06B0", 0, drop, ECU.ALL, False), + OBDCommand("MON_PM_FILTER_B2" , "PM Filter Monitor Bank 2" , b"06B1", 0, drop, ECU.ALL, False), +] + __mode7__ = [ # name description cmd bytes decoder ECU fast OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , b"07", 0, dtc, ECU.ALL, False), @@ -214,7 +316,7 @@ def __init__(self): __mode3__, __mode4__, [], - [], + __mode6__, __mode7__, [], __mode9__, @@ -223,7 +325,8 @@ def __init__(self): # allow commands to be accessed by name for m in self.modes: for c in m: - self.__dict__[c.name] = c + if c is not None: + self.__dict__[c.name] = c for c in __misc__: self.__dict__[c.name] = c @@ -320,7 +423,10 @@ def has_pid(self, mode, pid): return False if pid >= len(self.modes[mode]): return False - return True + + # make sure that the command isn't reserved + return (self.modes[mode][pid] is not None) + else: logger.warning("has_pid() only accepts integer values for mode and PID") return False From 8d167930170e4aa6eed63cfaf5b0c4c5a00d0bfd Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 22:54:09 -0400 Subject: [PATCH 364/569] use pid decoder for MID getters --- obd/commands.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 698f4078..db5f6c2e 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -180,8 +180,10 @@ ] __mode6__ = [ + # Mode 06 calls PID's MID's (Monitor ID) + # This is for CAN only # name description cmd bytes decoder ECU fast - OBDCommand("MIDS_A" , "Supported MIDs [01-20]" , b"0600", 0, drop, ECU.ALL, False), + OBDCommand("MIDS_A" , "Supported MIDs [01-20]" , b"0600", 0, pid, ECU.ALL, False), OBDCommand("MON_O2_B1S1" , "O2 Sensor Monitor Bank 1 - Sensor 1" , b"0601", 0, drop, ECU.ALL, False), OBDCommand("MON_O2_B1S2" , "O2 Sensor Monitor Bank 1 - Sensor 2" , b"0602", 0, drop, ECU.ALL, False), OBDCommand("MON_O2_B1S3" , "O2 Sensor Monitor Bank 1 - Sensor 3" , b"0603", 0, drop, ECU.ALL, False), @@ -199,7 +201,7 @@ OBDCommand("MON_O2_B4S3" , "O2 Sensor Monitor Bank 4 - Sensor 3" , b"060F", 0, drop, ECU.ALL, False), OBDCommand("MON_O2_B4S4" , "O2 Sensor Monitor Bank 4 - Sensor 4" , b"0610", 0, drop, ECU.ALL, False), ] + ([None] * 15) + [ # 11 - 1F Reserved - OBDCommand("MIDS_B" , "Supported MIDs [21-40]" , b"0620", 0, drop, ECU.ALL, False), + OBDCommand("MIDS_B" , "Supported MIDs [21-40]" , b"0620", 0, pid, ECU.ALL, False), OBDCommand("MON_CATALYST_B1" , "Catalyst Monitor Bank 1" , b"0621", 0, drop, ECU.ALL, False), OBDCommand("MON_CATALYST_B2" , "Catalyst Monitor Bank 2" , b"0622", 0, drop, ECU.ALL, False), OBDCommand("MON_CATALYST_B3" , "Catalyst Monitor Bank 3" , b"0623", 0, drop, ECU.ALL, False), @@ -219,7 +221,7 @@ OBDCommand("MON_EVAP_020" , "EVAP Monitor (0.020\")" , b"063C", 0, drop, ECU.ALL, False), OBDCommand("MON_PURGE_FLOW" , "Purge Flow Monitor" , b"063D", 0, drop, ECU.ALL, False), ] + ([None] * 2) + [ # 3E - 3F Reserved - OBDCommand("MIDS_C" , "Supported MIDs [41-60]" , b"0640", 0, drop, ECU.ALL, False), + OBDCommand("MIDS_C" , "Supported MIDs [41-60]" , b"0640", 0, pid, ECU.ALL, False), OBDCommand("MON_O2_HEATER_B1S1" , "O2 Sensor Heater Monitor Bank 1 - Sensor 1" , b"0641", 0, drop, ECU.ALL, False), OBDCommand("MON_O2_HEATER_B1S2" , "O2 Sensor Heater Monitor Bank 1 - Sensor 2" , b"0642", 0, drop, ECU.ALL, False), OBDCommand("MON_O2_HEATER_B1S3" , "O2 Sensor Heater Monitor Bank 1 - Sensor 3" , b"0643", 0, drop, ECU.ALL, False), @@ -237,7 +239,7 @@ OBDCommand("MON_O2_HEATER_B4S3" , "O2 Sensor Heater Monitor Bank 4 - Sensor 3" , b"064F", 0, drop, ECU.ALL, False), OBDCommand("MON_O2_HEATER_B4S4" , "O2 Sensor Heater Monitor Bank 4 - Sensor 4" , b"0650", 0, drop, ECU.ALL, False), ] + ([None] * 15) + [ # 51 - 5F Reserved - OBDCommand("MIDS_D" , "Supported MIDs [61-80]" , b"0660", 0, drop, ECU.ALL, False), + OBDCommand("MIDS_D" , "Supported MIDs [61-80]" , b"0660", 0, pid, ECU.ALL, False), OBDCommand("MON_HEATED_CATALYST_B1" , "Heated Catalyst Monitor Bank 1" , b"0661", 0, drop, ECU.ALL, False), OBDCommand("MON_HEATED_CATALYST_B2" , "Heated Catalyst Monitor Bank 2" , b"0662", 0, drop, ECU.ALL, False), OBDCommand("MON_HEATED_CATALYST_B3" , "Heated Catalyst Monitor Bank 3" , b"0663", 0, drop, ECU.ALL, False), @@ -248,7 +250,7 @@ OBDCommand("MON_SECONDARY_AIR_3" , "Secondary Air Monitor 3" , b"0673", 0, drop, ECU.ALL, False), OBDCommand("MON_SECONDARY_AIR_4" , "Secondary Air Monitor 4" , b"0674", 0, drop, ECU.ALL, False), ] + ([None] * 11) + [ # 75 - 7F Reserved - OBDCommand("MIDS_E" , "Supported MIDs [81-A0]" , b"0680", 0, drop, ECU.ALL, False), + OBDCommand("MIDS_E" , "Supported MIDs [81-A0]" , b"0680", 0, pid, ECU.ALL, False), OBDCommand("MON_FUEL_SYSTEM_B1" , "Fuel System Monitor Bank 1" , b"0681", 0, drop, ECU.ALL, False), OBDCommand("MON_FUEL_SYSTEM_B2" , "Fuel System Monitor Bank 2" , b"0682", 0, drop, ECU.ALL, False), OBDCommand("MON_FUEL_SYSTEM_B3" , "Fuel System Monitor Bank 3" , b"0683", 0, drop, ECU.ALL, False), @@ -262,7 +264,7 @@ OBDCommand("MON_NOX_CATALYST_B1" , "NOx Catalyst Monitor Bank 1" , b"0698", 0, drop, ECU.ALL, False), OBDCommand("MON_NOX_CATALYST_B2" , "NOx Catalyst Monitor Bank 2" , b"0699", 0, drop, ECU.ALL, False), ] + ([None] * 6) + [ # 9A - 9F Reserved - OBDCommand("MIDS_F" , "Supported MIDs [A1-C0]" , b"06A0", 0, drop, ECU.ALL, False), + OBDCommand("MIDS_F" , "Supported MIDs [A1-C0]" , b"06A0", 0, pid, ECU.ALL, False), OBDCommand("MON_MISFIRE_GENERAL" , "Misfire Monitor General Data" , b"06A1", 0, drop, ECU.ALL, False), OBDCommand("MON_MISFIRE_CYLINDER_1" , "Misfire Cylinder 1 Data" , b"06A2", 0, drop, ECU.ALL, False), OBDCommand("MON_MISFIRE_CYLINDER_2" , "Misfire Cylinder 2 Data" , b"06A3", 0, drop, ECU.ALL, False), From 2b58457b859875868a4e1a304ef08bd50ab9aaa4 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 23:05:57 -0400 Subject: [PATCH 365/569] fixed tests for None reserved commands, and mode_int -> mode --- obd/OBDCommand.py | 4 ++-- obd/commands.py | 13 +++++++++---- obd/obd.py | 4 ++-- tests/test_OBDCommand.py | 16 ++++++++-------- tests/test_commands.py | 33 +++++++++++++++++++++++++-------- 5 files changed, 46 insertions(+), 24 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 0fc023ab..c007816a 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -65,14 +65,14 @@ def clone(self): self.fast) @property - def mode_int(self): + def mode(self): if len(self.command) >= 2: return unhex(self.command[:2]) else: return 0 @property - def pid_int(self): + def pid(self): if len(self.command) > 2: return unhex(self.command[2:]) else: diff --git a/obd/commands.py b/obd/commands.py index db5f6c2e..04739de0 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -382,10 +382,15 @@ def base_commands(self): def pid_getters(self): """ returns a list of PID GET commands """ getters = [] - for m in self.modes: - for c in m: - if c.decode == pid: # GET commands have a special decoder - getters.append(c) + for mode in self.modes: + for cmd in mode: + + if cmd is None: + continue # this command is reserved + + if cmd.decode == pid: # GET commands have a special decoder + getters.append(cmd) + return getters diff --git a/obd/obd.py b/obd/obd.py index a3004f73..8efbb52b 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -120,8 +120,8 @@ def __load_commands(self): for i in range(len(supported)): if supported[i] == "1": - mode = get.mode_int - pid = get.pid_int + i + 1 + mode = get.mode + pid = get.pid + i + 1 if commands.has_pid(mode, pid): self.supported_commands.add(commands[mode][pid]) diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index b0bb4aa7..bad9b8b8 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -19,8 +19,8 @@ def test_constructor(): assert cmd.ecu == ECU.ENGINE assert cmd.fast == False - assert cmd.mode_int == 1 - assert cmd.pid_int == 35 + assert cmd.mode == 1 + assert cmd.pid == 35 # a case where "fast", and "supported" were set explicitly # name description cmd bytes decoder ECU fast @@ -67,18 +67,18 @@ def test_call(): -def test_get_mode_int(): +def test_get_mode(): cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) - assert cmd.mode_int == 0x01 + assert cmd.mode == 0x01 cmd = OBDCommand("", "", "", "23", 4, noop, ECU.ENGINE) - assert cmd.mode_int == 0 + assert cmd.mode == 0 -def test_pid_int(): +def test_pid(): cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) - assert cmd.pid_int == 0x23 + assert cmd.pid == 0x23 cmd = OBDCommand("", "", "01", 4, noop, ECU.ENGINE) - assert cmd.pid_int == 0 + assert cmd.pid == 0 diff --git a/tests/test_commands.py b/tests/test_commands.py index 0ac0df2f..dac06ade 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -7,11 +7,14 @@ def test_list_integrity(): for mode, cmds in enumerate(obd.commands.modes): for pid, cmd in enumerate(cmds): + if cmd is None: + continue # this command is reserved + assert cmd.command != "", "The Command's command string must not be null" # make sure the command tables are in mode & PID order - assert mode == cmd.mode_int, "Command is in the wrong mode list: %s" % cmd.name - assert pid == cmd.pid_int, "The index in the list must also be the PID: %s" % cmd.name + assert mode == cmd.mode, "Command is in the wrong mode list: %s" % cmd.name + assert pid == cmd.pid, "The index in the list must also be the PID: %s" % cmd.name # make sure all the fields are set assert cmd.name != "", "Command names must not be null" @@ -30,6 +33,10 @@ def test_unique_names(): for cmds in obd.commands.modes: for cmd in cmds: + + if cmd is None: + continue # this command is reserved + assert not names.__contains__(cmd.name), "Two commands share the same name: %s" % cmd.name names[cmd.name] = True @@ -39,9 +46,12 @@ def test_getitem(): for cmds in obd.commands.modes: for cmd in cmds: + if cmd is None: + continue # this command is reserved + # by [mode][pid] - mode = cmd.mode_int - pid = cmd.pid_int + mode = cmd.mode + pid = cmd.pid assert cmd == obd.commands[mode][pid], "mode %d, PID %d could not be accessed through __getitem__" % (mode, pid) # by [name] @@ -53,12 +63,15 @@ def test_contains(): for cmds in obd.commands.modes: for cmd in cmds: + if cmd is None: + continue # this command is reserved + # by (command) assert obd.commands.has_command(cmd) # by (mode, pid) - mode = cmd.mode_int - pid = cmd.pid_int + mode = cmd.mode + pid = cmd.pid assert obd.commands.has_pid(mode, pid) # by (name) @@ -78,7 +91,11 @@ def test_pid_getters(): # ensure that all pid getters are found pid_getters = obd.commands.pid_getters() - for cmds in obd.commands.modes: - for cmd in cmds: + for mode in obd.commands.modes: + for cmd in mode: + + if cmd is None: + continue # this command is reserved + if cmd.decode == pid: assert cmd in pid_getters From a6440158e2ab68da793790628f5d18dc57929985 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 29 Jun 2016 23:18:14 -0400 Subject: [PATCH 366/569] added TID table --- obd/codes.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/obd/codes.py b/obd/codes.py index 8278d922..ca394277 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -2205,3 +2205,20 @@ "Hybrid Regenerative", "Bifuel running diesel", ] + +TEST_IDS = { + # : + # 0x0 is reserved + 0x1 : ("rtl_threshold voltage", "Rich to lean sensor threshold voltage"), + 0x2 : ("ltr_threshold voltage", "Lean to rich sensor threshold voltage"), + 0x3 : ("low_voltage_switch_time", "Low sensor voltage for switch time calculation"), + 0x4 : ("high_voltage_switch_time", "High sensor voltage for switch time calculation"), + 0x5 : ("rtl_switch_time", "Rich to lean sensor switch time"), + 0x6 : ("ltr_switch_time", "Lean to rich sensor switch time"), + 0x7 : ("min_voltage", "Minimum sensor voltage for test cycle"), + 0x8 : ("max_voltage", "Maximum sensor voltage for test cycle"), + 0x9 : ("transition_time", "Time between sensor transitions"), + 0xA : ("sensor_period", "Sensor period"), + 0xB : ("misfire_average", "Average misfire counts for last ten driving cycles"), + 0xC : ("misfire_count", "Misfire counts for last/current driving cycles"), +} From 1abddb87eb4b5e662663a0ec2219e2bb9dd02af1 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 01:24:21 -0400 Subject: [PATCH 367/569] started implementing monitor decoders and UAS system --- obd/OBDResponse.py | 18 +++++++++++++++ obd/decoders.py | 32 ++++++++++++++++++++++++++- obd/uas.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 obd/uas.py diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py index 073dbb91..ef09b0ba 100644 --- a/obd/OBDResponse.py +++ b/obd/OBDResponse.py @@ -33,6 +33,7 @@ import time import pint +from .codes import * # export the unit registry @@ -93,3 +94,20 @@ def __str__(self): a = "Available" if self.available else "Unavailable" c = "Incomplete" if self.incomplete else "Complete" return "Test %s: %s, %s" % (self.name, a, c) + + +class Monitor(): + def __init__(self): + # make all TID tests available as properties + for tid in TEST_IDS: + self.__dict__[TEST_IDS[tid][0]] = MonitorTest() + + +class MonitorTest(): + def __init__(self): + self.tid = None + self.name = None + self.desc = None + self.value = None + self.min = None + self.max = None diff --git a/obd/decoders.py b/obd/decoders.py index 7f8baa2e..8c8c72d0 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -32,7 +32,7 @@ import math from .utils import * from .codes import * -from .OBDResponse import Unit, Status, Test +from .OBDResponse import Unit, Status, Test, Monitor, MonitorTest import logging @@ -425,3 +425,33 @@ def dtc(messages): codes.append( (dtc, desc) ) return codes + + +def monitor_test(d): + test = MonitorTest() + test.tid = bytes_to_int(test_data[1]) + test.name = TEST_IDS[test.tid][0] # lookup the description from the table + test.desc = TEST_IDS[test.tid][1] # lookup the description from the table + + + + return test + + +def monitor(messages): + d = messages[0].data + mon = Monitor() + + # test that we got the right number of bytes + extra_bytes = len(d) % 9 + + if extra_bytes != 0: + logger.debug("Encountered monitor message with non-multiple of 9 bytes. Truncating...") + d = d[:len(d) - extra_bytes] + + # look at data in blocks of 9 bytes (one test result) + for n in range(0, len(d), 9): + test = monitor_test(d[n:n + 8]) # extract the 9 byte block, and parse a new MonitorTest + setattr(mon,test.name, test) # use the "name" field as the property + + return mon diff --git a/obd/uas.py b/obd/uas.py new file mode 100644 index 00000000..78655f37 --- /dev/null +++ b/obd/uas.py @@ -0,0 +1,55 @@ + +######################################################################## +# # +# python-OBD: A python OBD-II serial module derived from pyobd # +# # +# Copyright 2004 Donour Sizemore (donour@uchicago.edu) # +# Copyright 2009 Secons Ltd. (www.obdtester.com) # +# Copyright 2009 Peter J. Creath # +# Copyright 2016 Brendan Whitfield (brendan-w.com) # +# # +######################################################################## +# # +# uac.py # +# # +# This file is part of python-OBD (a derivative of pyOBD) # +# # +# python-OBD is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 2 of the License, or # +# (at your option) any later version. # +# # +# python-OBD is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with python-OBD. If not, see . # +# # +######################################################################## + +from .utils import * +from .OBDResponse import Unit + + +class UAS(): + """ + Class for representing a Unit and Scale conversion + Used in the decoding of Mode 06 monitor responses + """ + + def __init__(self, signed, scale, unit): + self.signed = signed + self.scale = scale + self.unit = unit + + def __call__(self, _bytes): + value = bytes_to_int(_bytes) + + if self.signed: + value = twos_comp(value, len(_bytes) * 8) + + value *= self.scale + + return Unit.Quantity(value, self.unit) From 0a227c0da50332ecd055d93bc709b6148a94e743 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 01:51:00 -0400 Subject: [PATCH 368/569] started filling in the UAS ID table --- obd/OBDResponse.py | 1 + obd/uas.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py index ef09b0ba..2417bae3 100644 --- a/obd/OBDResponse.py +++ b/obd/OBDResponse.py @@ -39,6 +39,7 @@ # export the unit registry Unit = pint.UnitRegistry() Unit.define("percent = [] = %") +Unit.define("ratio = []") Unit.define("gps = gram / second = GPS = grams_per_second") Unit.define("lph = liter / hour = LPH = liters_per_hour") diff --git a/obd/uas.py b/obd/uas.py index 78655f37..42ae9883 100644 --- a/obd/uas.py +++ b/obd/uas.py @@ -53,3 +53,48 @@ def __call__(self, _bytes): value *= self.scale return Unit.Quantity(value, self.unit) + + +# dict for looking up standardized UAS IDs with conversion objects +UAS_IDS = { + 0x01 : UAS(False, 1, Unit.count), + 0x02 : UAS(False, 0.1, Unit.count), + 0x03 : UAS(False, 0.01, Unit.count), + 0x04 : UAS(False, 0.001, Unit.count), + 0x05 : UAS(False, 0.0000305, Unit.count), + 0x06 : UAS(False, 0.000305, Unit.count), + 0x07 : UAS(False, 0.25, Unit.rpm), + 0x08 : UAS(False, 0.01, Unit.kph), + 0x09 : UAS(False, 1, Unit.kph), + 0x0A : UAS(False, 0.122, Unit.millivolt), + 0x0B : UAS(False, 0.001, Unit.volt), + 0x0C : UAS(False, 0.01, Unit.volt), + 0x0D : UAS(False, 0.00390625, Unit.milliampere), + 0x0E : UAS(False, 0.001, Unit.ampere), + 0x0F : UAS(False, 0.01, Unit.ampere), + 0x10 : UAS(False, 1, Unit.millisecond), + 0x11 : UAS(False, 100, Unit.millisecond), + 0x12 : UAS(False, 1, Unit.second), + 0x13 : UAS(False, 1, Unit.milliohm), + 0x14 : UAS(False, 1, Unit.ohm), + 0x15 : UAS(False, 1, Unit.kiloohm), + 0x16 : None, # TODO + 0x17 : UAS(False, 0.01, Unit.kilopascal), + 0x18 : UAS(False, 0.0117, Unit.kilopascal), + 0x19 : UAS(False, 0.079, Unit.kilopascal), + 0x1A : UAS(False, 1, Unit.kilopascal), + 0x1B : UAS(False, 10, Unit.kilopascal), + 0x1C : UAS(False, 0.01, Unit.degree), + 0x1D : UAS(False, 0.5, Unit.degree), + 0x1E : None, # TODO + 0x1F : UAS(False, 0.05, Unit.ratio), + 0x20 : UAS(False, 0.00390625, Unit.ratio), + 0x21 : UAS(False, 1, Unit.millihertz), + 0x22 : UAS(False, 1, Unit.hertz), + 0x23 : UAS(False, 1, Unit.kilohertz), + 0x24 : UAS(False, 1, Unit.count), + 0x25 : UAS(False, 1, Unit.kilometer), + 0x26 : UAS(False, 0.1, Unit.millivolt / Unit.millisecond), + 0x27 : UAS(False, 0.1, Unit.grams_per_second), + 0x28 : UAS(False, 1, Unit.grams_per_second), +} From 0d3dcfb1678d9d5e531cc43c68aed43b133fa312 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 12:31:11 -0400 Subject: [PATCH 369/569] special handling for mode 6 in CAN protocol --- obd/protocols/protocol_can.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index ab1f34f7..8d4b2191 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -264,6 +264,8 @@ def parse_message(self, message): message.data = message.data[:ff[0].data_len] + # TODO: this is an ugly solution, maybe move mode/pid byte ignoring to the decoders? + # chop off the Mode/PID bytes based on the mode number mode = message.data[0] if mode == 0x43: @@ -278,6 +280,12 @@ def parse_message(self, message): # skip the PID byte and the DTC count, message.data = message.data[2:][:num_dtc_bytes] + elif mode == 0x46: + # the monitor test mode only has a mode number + # the MID (mode 6's version of a PID) is repeated, + # and handled in the decoder + message.data = message.data[1:] + else: # skip the Mode and PID bytes # From e78045ea041e58607c27dcf5c40a04436b9b4fe1 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 12:47:31 -0400 Subject: [PATCH 370/569] implemented __str__ for monitors --- obd/OBDResponse.py | 27 +++++++++++++++++++++++++-- obd/codes.py | 24 ++++++++++++------------ obd/decoders.py | 1 - 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py index 2417bae3..d154b2bb 100644 --- a/obd/OBDResponse.py +++ b/obd/OBDResponse.py @@ -101,14 +101,37 @@ class Monitor(): def __init__(self): # make all TID tests available as properties for tid in TEST_IDS: - self.__dict__[TEST_IDS[tid][0]] = MonitorTest() + name = TEST_IDS[tid][0] + self.__dict__[name] = MonitorTest() + + def __str__(self): + output = "" + + for tid in TEST_IDS: + name = TEST_IDS[tid][0] + test = self.__dict__[name] + if not test.is_null(): + output += str(test) + "\n" + + return output class MonitorTest(): def __init__(self): self.tid = None - self.name = None self.desc = None self.value = None self.min = None self.max = None + + @property + def passed(self): + return (self.value >= self.min) and (self.value <= self.max) + + def is_null(self): + return self.tid is None or self.value is None + + def __str__(self): + return "%s : %s [%s]" % (self.desc, + str(self.value), + "PASSED" if self.passed else "FAILED") diff --git a/obd/codes.py b/obd/codes.py index ca394277..c453a139 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -2209,16 +2209,16 @@ TEST_IDS = { # : # 0x0 is reserved - 0x1 : ("rtl_threshold voltage", "Rich to lean sensor threshold voltage"), - 0x2 : ("ltr_threshold voltage", "Lean to rich sensor threshold voltage"), - 0x3 : ("low_voltage_switch_time", "Low sensor voltage for switch time calculation"), - 0x4 : ("high_voltage_switch_time", "High sensor voltage for switch time calculation"), - 0x5 : ("rtl_switch_time", "Rich to lean sensor switch time"), - 0x6 : ("ltr_switch_time", "Lean to rich sensor switch time"), - 0x7 : ("min_voltage", "Minimum sensor voltage for test cycle"), - 0x8 : ("max_voltage", "Maximum sensor voltage for test cycle"), - 0x9 : ("transition_time", "Time between sensor transitions"), - 0xA : ("sensor_period", "Sensor period"), - 0xB : ("misfire_average", "Average misfire counts for last ten driving cycles"), - 0xC : ("misfire_count", "Misfire counts for last/current driving cycles"), + 0x01 : ("rtl_threshold voltage", "Rich to lean sensor threshold voltage"), + 0x02 : ("ltr_threshold voltage", "Lean to rich sensor threshold voltage"), + 0x03 : ("low_voltage_switch_time", "Low sensor voltage for switch time calculation"), + 0x04 : ("high_voltage_switch_time", "High sensor voltage for switch time calculation"), + 0x05 : ("rtl_switch_time", "Rich to lean sensor switch time"), + 0x06 : ("ltr_switch_time", "Lean to rich sensor switch time"), + 0x07 : ("min_voltage", "Minimum sensor voltage for test cycle"), + 0x08 : ("max_voltage", "Maximum sensor voltage for test cycle"), + 0x09 : ("transition_time", "Time between sensor transitions"), + 0x0A : ("sensor_period", "Sensor period"), + 0x0B : ("misfire_average", "Average misfire counts for last ten driving cycles"), + 0x0C : ("misfire_count", "Misfire counts for last/current driving cycles"), } diff --git a/obd/decoders.py b/obd/decoders.py index 8c8c72d0..6f4b6601 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -430,7 +430,6 @@ def dtc(messages): def monitor_test(d): test = MonitorTest() test.tid = bytes_to_int(test_data[1]) - test.name = TEST_IDS[test.tid][0] # lookup the description from the table test.desc = TEST_IDS[test.tid][1] # lookup the description from the table From c492272752dc01f689a5d04908f08af55ef5260e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 12:52:33 -0400 Subject: [PATCH 371/569] simplified __str__ code --- obd/OBDResponse.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py index d154b2bb..ba9cc5d4 100644 --- a/obd/OBDResponse.py +++ b/obd/OBDResponse.py @@ -99,21 +99,18 @@ def __str__(self): class Monitor(): def __init__(self): + self.tests = [] + # make all TID tests available as properties for tid in TEST_IDS: name = TEST_IDS[tid][0] - self.__dict__[name] = MonitorTest() + test = MonitorTest() + self.__dict__[name] = test + self.tests.append(test) def __str__(self): - output = "" - - for tid in TEST_IDS: - name = TEST_IDS[tid][0] - test = self.__dict__[name] - if not test.is_null(): - output += str(test) + "\n" - - return output + valid_tests = [str(test) for test in tests if not test.is_null()] + return "\n".join(valid_tests) class MonitorTest(): From 4a37c7b6ff24f966b479c339d795c6386161a836 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 12:58:16 -0400 Subject: [PATCH 372/569] moved UnitRegistry to the UAS file --- obd/OBDResponse.py | 10 ---------- obd/{uas.py => UnitsAndScaling.py} | 13 +++++++++++-- obd/__init__.py | 3 ++- obd/decoders.py | 5 ++--- 4 files changed, 15 insertions(+), 16 deletions(-) rename obd/{uas.py => UnitsAndScaling.py} (93%) diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py index ba9cc5d4..b5dbfea5 100644 --- a/obd/OBDResponse.py +++ b/obd/OBDResponse.py @@ -30,20 +30,10 @@ ######################################################################## - import time -import pint from .codes import * -# export the unit registry -Unit = pint.UnitRegistry() -Unit.define("percent = [] = %") -Unit.define("ratio = []") -Unit.define("gps = gram / second = GPS = grams_per_second") -Unit.define("lph = liter / hour = LPH = liters_per_hour") - - class OBDResponse(): """ Standard response object for any OBDCommand """ diff --git a/obd/uas.py b/obd/UnitsAndScaling.py similarity index 93% rename from obd/uas.py rename to obd/UnitsAndScaling.py index 42ae9883..3c2e8875 100644 --- a/obd/uas.py +++ b/obd/UnitsAndScaling.py @@ -10,7 +10,7 @@ # # ######################################################################## # # -# uac.py # +# UnitsAndScaling.py # # # # This file is part of python-OBD (a derivative of pyOBD) # # # @@ -29,8 +29,17 @@ # # ######################################################################## +import pint from .utils import * -from .OBDResponse import Unit + + +# export the unit registry +Unit = pint.UnitRegistry() +Unit.define("percent = [] = %") +Unit.define("ratio = []") +Unit.define("gps = gram / second = GPS = grams_per_second") +Unit.define("lph = liter / hour = LPH = liters_per_hour") + class UAS(): diff --git a/obd/__init__.py b/obd/__init__.py index 1e7851f4..6781af6c 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -41,9 +41,10 @@ from .async import Async from .commands import commands from .OBDCommand import OBDCommand -from .OBDResponse import OBDResponse, Unit +from .OBDResponse import OBDResponse from .protocols import ECU from .utils import scan_serial, scanSerial, OBDStatus # TODO: scanSerial() deprecated +from .UnitsAndScaling import Unit import logging diff --git a/obd/decoders.py b/obd/decoders.py index 6f4b6601..319a53d9 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -32,7 +32,8 @@ import math from .utils import * from .codes import * -from .OBDResponse import Unit, Status, Test, Monitor, MonitorTest +from .OBDResponse import Status, Test, Monitor, MonitorTest +from .UnitsAndScaling import Unit import logging @@ -432,8 +433,6 @@ def monitor_test(d): test.tid = bytes_to_int(test_data[1]) test.desc = TEST_IDS[test.tid][1] # lookup the description from the table - - return test From 000095ccbc23ae61a638c42b1c34fbfb1d663b41 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 13:38:19 -0400 Subject: [PATCH 373/569] finished first pass of UAS ID table --- obd/UnitsAndScaling.py | 64 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/obd/UnitsAndScaling.py b/obd/UnitsAndScaling.py index 3c2e8875..23232643 100644 --- a/obd/UnitsAndScaling.py +++ b/obd/UnitsAndScaling.py @@ -39,6 +39,7 @@ Unit.define("ratio = []") Unit.define("gps = gram / second = GPS = grams_per_second") Unit.define("lph = liter / hour = LPH = liters_per_hour") +Unit.define("ppm = count / 1000000 = PPM = parts_per_million") @@ -66,6 +67,7 @@ def __call__(self, _bytes): # dict for looking up standardized UAS IDs with conversion objects UAS_IDS = { + # unsigned ----------------------------------------- 0x01 : UAS(False, 1, Unit.count), 0x02 : UAS(False, 0.1, Unit.count), 0x03 : UAS(False, 0.01, Unit.count), @@ -106,4 +108,66 @@ def __call__(self, _bytes): 0x26 : UAS(False, 0.1, Unit.millivolt / Unit.millisecond), 0x27 : UAS(False, 0.1, Unit.grams_per_second), 0x28 : UAS(False, 1, Unit.grams_per_second), + 0x29 : UAS(False, 0.25, Unit.pascal / Unit.second), + 0x2A : UAS(False, 0.001, Unit.kilogram / Unit.hour), + 0x2B : UAS(False, 1, Unit.count), + 0x2C : UAS(False, 0.01, Unit.gram), # TODO: per-cylinder + 0x2D : UAS(False, 0.01, Unit.milligram), # TODO: per-stroke + 0x2E : None, # TODO: True/False + 0x2F : UAS(False, 0.01, Unit.percent), + 0x30 : UAS(False, 0.001526, Unit.percent), + 0x31 : UAS(False, 0.001, Unit.liter), + 0x32 : UAS(False, 0.0000305, Unit.inch), + 0x33 : UAS(False, 0.00024414, Unit.count), # TODO: equivalence ration (lambda) + 0x34 : UAS(False, 1, Unit.minute), + 0x35 : UAS(False, 10, Unit.millisecond), + 0x36 : UAS(False, 0.01, Unit.gram), + 0x37 : UAS(False, 0.1, Unit.gram), + 0x38 : UAS(False, 1, Unit.gram), + 0x39 : UAS(False, 0.01, Unit.percent), # TODO: centered + 0x3A : UAS(False, 0.001, Unit.gram), + 0x3B : UAS(False, 0.0001, Unit.gram), + 0x3C : UAS(False, 0.1, Unit.microsecond), + 0x3D : UAS(False, 0.01, Unit.milliampere), + 0x3E : UAS(False, 0.00006103516, Unit.millimeter ** 2), + 0x3F : UAS(False, 0.01, Unit.liter), + 0x40 : UAS(False, 1, Unit.ppm), + 0x41 : UAS(False, 0.1, Unit.microampere), + + # signed ----------------------------------------- + 0x81 : UAS(True, 1, Unit.count), + 0x82 : UAS(True, 0.1, Unit.count), + 0x83 : UAS(True, 0.01, Unit.count), + 0x84 : UAS(True, 0.001, Unit.count), + 0x85 : UAS(True, 0.0000305, Unit.count), + 0x86 : UAS(True, 0.000305, Unit.count), + 0x87 : UAS(True, 1, Unit.ppm), + # + 0x8A : UAS(True, 0.122, Unit.millivolt), + 0x8B : UAS(True, 0.001, Unit.volt), + 0x8C : UAS(True, 0.01, Unit.volt), + 0x8D : UAS(True, 0.00390625, Unit.milliampere), + 0x8E : UAS(True, 0.001, Unit.ampere), + # + 0x90 : UAS(True, 1, Unit.millisecond), + # + 0x96 : UAS(True, 0.1, Unit.celsius), + # + 0x99 : UAS(True, 0.1, Unit.kilopascal), + # + 0x9C : UAS(True, 0.01, Unit.degree), + 0x9D : UAS(True, 0.5, Unit.degree), + # + 0xA8 : UAS(True, 1, Unit.grams_per_second), + 0xA9 : UAS(True, 0.25, Unit.pascal / Unit.second), + # + 0xAD : UAS(True, 0.01, Unit.milligram), # TODO: per-stroke + 0xAE : UAS(True, 0.1, Unit.milligram), # TODO: per-stroke + 0xAF : UAS(True, 0.01, Unit.percent), + 0xB0 : UAS(True, 0.003052, Unit.percent), + 0xB1 : UAS(True, 2, Unit.millivolt / Unit.second), + # + 0xFC : UAS(True, 0.01, Unit.kilopascal), + 0xFD : UAS(True, 0.001, Unit.kilopascal), + 0xFE : UAS(True, 0.25, Unit.pascal), } From d826a219a255d56576d56827f26c14aedfd09427 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 13:48:06 -0400 Subject: [PATCH 374/569] finished remaining conversions --- obd/UnitsAndScaling.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/obd/UnitsAndScaling.py b/obd/UnitsAndScaling.py index 23232643..04ca0634 100644 --- a/obd/UnitsAndScaling.py +++ b/obd/UnitsAndScaling.py @@ -49,10 +49,11 @@ class UAS(): Used in the decoding of Mode 06 monitor responses """ - def __init__(self, signed, scale, unit): + def __init__(self, signed, scale, unit, offset=0): self.signed = signed self.scale = scale self.unit = unit + self.offset = offset def __call__(self, _bytes): value = bytes_to_int(_bytes) @@ -61,7 +62,7 @@ def __call__(self, _bytes): value = twos_comp(value, len(_bytes) * 8) value *= self.scale - + value += self.offset return Unit.Quantity(value, self.unit) @@ -89,7 +90,7 @@ def __call__(self, _bytes): 0x13 : UAS(False, 1, Unit.milliohm), 0x14 : UAS(False, 1, Unit.ohm), 0x15 : UAS(False, 1, Unit.kiloohm), - 0x16 : None, # TODO + 0x16 : UAS(False, 0.1, Unit.celsius, offset=-40.0), 0x17 : UAS(False, 0.01, Unit.kilopascal), 0x18 : UAS(False, 0.0117, Unit.kilopascal), 0x19 : UAS(False, 0.079, Unit.kilopascal), @@ -97,7 +98,7 @@ def __call__(self, _bytes): 0x1B : UAS(False, 10, Unit.kilopascal), 0x1C : UAS(False, 0.01, Unit.degree), 0x1D : UAS(False, 0.5, Unit.degree), - 0x1E : None, # TODO + 0x1E : UAS(False, 0.0000305, Unit.ratio), 0x1F : UAS(False, 0.05, Unit.ratio), 0x20 : UAS(False, 0.00390625, Unit.ratio), 0x21 : UAS(False, 1, Unit.millihertz), @@ -111,20 +112,20 @@ def __call__(self, _bytes): 0x29 : UAS(False, 0.25, Unit.pascal / Unit.second), 0x2A : UAS(False, 0.001, Unit.kilogram / Unit.hour), 0x2B : UAS(False, 1, Unit.count), - 0x2C : UAS(False, 0.01, Unit.gram), # TODO: per-cylinder - 0x2D : UAS(False, 0.01, Unit.milligram), # TODO: per-stroke - 0x2E : None, # TODO: True/False + 0x2C : UAS(False, 0.01, Unit.gram), # per-cylinder + 0x2D : UAS(False, 0.01, Unit.milligram), # per-stroke + 0x2E : lambda _bytes: any([ bool(x) for x in _bytes]) 0x2F : UAS(False, 0.01, Unit.percent), 0x30 : UAS(False, 0.001526, Unit.percent), 0x31 : UAS(False, 0.001, Unit.liter), 0x32 : UAS(False, 0.0000305, Unit.inch), - 0x33 : UAS(False, 0.00024414, Unit.count), # TODO: equivalence ration (lambda) + 0x33 : UAS(False, 0.00024414, Unit.ratio), 0x34 : UAS(False, 1, Unit.minute), 0x35 : UAS(False, 10, Unit.millisecond), 0x36 : UAS(False, 0.01, Unit.gram), 0x37 : UAS(False, 0.1, Unit.gram), 0x38 : UAS(False, 1, Unit.gram), - 0x39 : UAS(False, 0.01, Unit.percent), # TODO: centered + 0x39 : UAS(False, 0.01, Unit.percent, offset=-327.68), 0x3A : UAS(False, 0.001, Unit.gram), 0x3B : UAS(False, 0.0001, Unit.gram), 0x3C : UAS(False, 0.1, Unit.microsecond), @@ -161,8 +162,8 @@ def __call__(self, _bytes): 0xA8 : UAS(True, 1, Unit.grams_per_second), 0xA9 : UAS(True, 0.25, Unit.pascal / Unit.second), # - 0xAD : UAS(True, 0.01, Unit.milligram), # TODO: per-stroke - 0xAE : UAS(True, 0.1, Unit.milligram), # TODO: per-stroke + 0xAD : UAS(True, 0.01, Unit.milligram), # per-stroke + 0xAE : UAS(True, 0.1, Unit.milligram), # per-stroke 0xAF : UAS(True, 0.01, Unit.percent), 0xB0 : UAS(True, 0.003052, Unit.percent), 0xB1 : UAS(True, 2, Unit.millivolt / Unit.second), From 6ced0a231728637a530e75b07c001aab983f9aa0 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 14:54:12 -0400 Subject: [PATCH 375/569] finished intial implementation of mode 06 decoder, fixed tests --- obd/UnitsAndScaling.py | 2 +- obd/decoders.py | 14 +++++++++++++- tests/test_OBDCommand.py | 2 +- tests/test_decoders.py | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/obd/UnitsAndScaling.py b/obd/UnitsAndScaling.py index 04ca0634..f80cb701 100644 --- a/obd/UnitsAndScaling.py +++ b/obd/UnitsAndScaling.py @@ -114,7 +114,7 @@ def __call__(self, _bytes): 0x2B : UAS(False, 1, Unit.count), 0x2C : UAS(False, 0.01, Unit.gram), # per-cylinder 0x2D : UAS(False, 0.01, Unit.milligram), # per-stroke - 0x2E : lambda _bytes: any([ bool(x) for x in _bytes]) + 0x2E : lambda _bytes: any([ bool(x) for x in _bytes]), 0x2F : UAS(False, 0.01, Unit.percent), 0x30 : UAS(False, 0.001526, Unit.percent), 0x31 : UAS(False, 0.001, Unit.liter), diff --git a/obd/decoders.py b/obd/decoders.py index 319a53d9..b2a5cff2 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -33,7 +33,7 @@ from .utils import * from .codes import * from .OBDResponse import Status, Test, Monitor, MonitorTest -from .UnitsAndScaling import Unit +from .UnitsAndScaling import Unit, UAS_IDS import logging @@ -430,9 +430,21 @@ def dtc(messages): def monitor_test(d): test = MonitorTest() + + uas = UAS_IDS.get(bytes_to_int(test_data[2]), None) + + # if we can't decode the value, return a null MonitorTest + if uas is None: + return test + test.tid = bytes_to_int(test_data[1]) test.desc = TEST_IDS[test.tid][1] # lookup the description from the table + # convert the value and limits to actual values + test.value = uas(test_data[3:5]) + test.min = uas(test_data[5:7]) + test.max = uas(test_data[7:]) + return test diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index bad9b8b8..d97c02ad 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -1,6 +1,6 @@ from obd.OBDCommand import OBDCommand -from obd.OBDResponse import Unit +from obd.UnitsAndScaling import Unit from obd.decoders import noop from obd.protocols import * diff --git a/tests/test_decoders.py b/tests/test_decoders.py index dffed7cf..598508cf 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -1,7 +1,7 @@ from binascii import unhexlify -from obd.OBDResponse import Unit +from obd.UnitsAndScaling import Unit from obd.protocols.protocol import Frame, Message import obd.decoders as d From 04c49051f6824c17984f7235d1b216f63f27d0c0 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 15:34:22 -0400 Subject: [PATCH 376/569] initial test of monitor decoder --- obd/OBDResponse.py | 18 ++++++++++++---- obd/decoders.py | 49 ++++++++++++++++++++++++------------------ tests/test_decoders.py | 6 ++++++ 3 files changed, 48 insertions(+), 25 deletions(-) diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py index b5dbfea5..9a9fd037 100644 --- a/obd/OBDResponse.py +++ b/obd/OBDResponse.py @@ -99,13 +99,17 @@ def __init__(self): self.tests.append(test) def __str__(self): - valid_tests = [str(test) for test in tests if not test.is_null()] - return "\n".join(valid_tests) + valid_tests = [str(test) for test in self.tests if not test.is_null()] + if len(valid_tests) > 0: + return "\n".join(valid_tests) + else: + return "No tests to report" class MonitorTest(): def __init__(self): self.tid = None + self.name = None self.desc = None self.value = None self.min = None @@ -113,10 +117,16 @@ def __init__(self): @property def passed(self): - return (self.value >= self.min) and (self.value <= self.max) + if not self.is_null(): + return (self.value >= self.min) and (self.value <= self.max) + else: + return False def is_null(self): - return self.tid is None or self.value is None + return (self.tid is None or + self.value is None or + self.min is None or + self.max is None) def __str__(self): return "%s : %s [%s]" % (self.desc, diff --git a/obd/decoders.py b/obd/decoders.py index b2a5cff2..3a376d4f 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -316,19 +316,19 @@ def fuel_status(messages): v = d[0] # todo, support second fuel system if v <= 0: - logger.warning("Invalid fuel status response (v <= 0)") + logger.debug("Invalid fuel status response (v <= 0)") return None i = math.log(v, 2) # only a single bit should be on if i % 1 != 0: - logger.warning("Invalid fuel status response (multiple bits set)") + logger.debug("Invalid fuel status response (multiple bits set)") return None i = int(i) if i >= len(FUEL_STATUS): - logger.warning("Invalid fuel status response (no table entry)") + logger.debug("Invalid fuel status response (no table entry)") return None return FUEL_STATUS[i] @@ -339,19 +339,19 @@ def air_status(messages): v = d[0] if v <= 0: - logger.warning("Invalid air status response (v <= 0)") + logger.debug("Invalid air status response (v <= 0)") return None i = math.log(v, 2) # only a single bit should be on if i % 1 != 0: - logger.warning("Invalid air status response (multiple bits set)") + logger.debug("Invalid air status response (multiple bits set)") return None i = int(i) if i >= len(AIR_STATUS): - logger.warning("Invalid air status response (no table entry)") + logger.debug("Invalid air status response (no table entry)") return None return AIR_STATUS[i] @@ -428,24 +428,32 @@ def dtc(messages): return codes -def monitor_test(d): - test = MonitorTest() +def parse_monitor_test(d, mon): + tid = d[1] - uas = UAS_IDS.get(bytes_to_int(test_data[2]), None) + if tid not in TEST_IDS: + logger.debug("Encountered unknown Test ID") + return # if it's an unknown TID, abort - # if we can't decode the value, return a null MonitorTest - if uas is None: - return test + name = TEST_IDS[tid][0] # lookup the name from the table + desc = TEST_IDS[tid][1] # lookup the description from the table + + test = mon.__dict__[name] # use the "name" field to lookup the right test - test.tid = bytes_to_int(test_data[1]) - test.desc = TEST_IDS[test.tid][1] # lookup the description from the table + uas = UAS_IDS.get(d[2], None) - # convert the value and limits to actual values - test.value = uas(test_data[3:5]) - test.min = uas(test_data[5:7]) - test.max = uas(test_data[7:]) + # if we can't decode the value, return a null MonitorTest + if uas is None: + logger.debug("Encountered unknown Units and Scaling ID") + return - return test + # load the test results + test.tid = tid + test.name = name + test.desc = desc + test.value = uas(d[3:5]) # convert bytes to actual values + test.min = uas(d[5:7]) + test.max = uas(d[7:]) def monitor(messages): @@ -461,7 +469,6 @@ def monitor(messages): # look at data in blocks of 9 bytes (one test result) for n in range(0, len(d), 9): - test = monitor_test(d[n:n + 8]) # extract the 9 byte block, and parse a new MonitorTest - setattr(mon,test.name, test) # use the "name" field as the property + parse_monitor_test(d[n:n + 9], mon) # extract the 9 byte block, and parse a new MonitorTest return mon diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 598508cf..f210f7ba 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -198,3 +198,9 @@ def test_dtc(): ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ("B0003", "Unknown error code"), ] + +def test_monitor(): + # v = d.monitor(m("01010A0BB00BB00BB00105100048000000640185240096004BFFFF")) + v = d.monitor(m("01010A0BB00BB00BB0")) + print(v) + assert(False) From 29ab768dc5b2bf88fd11b0f9c7f53424e960582d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 15:59:24 -0400 Subject: [PATCH 377/569] better implemenation and lookup tools for MonitorTests --- obd/OBDResponse.py | 33 +++++++++++++++++++++++++-------- obd/__init__.py | 2 +- obd/decoders.py | 28 ++++++++++++++++------------ tests/test_decoders.py | 4 ++-- 4 files changed, 44 insertions(+), 23 deletions(-) diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py index 9a9fd037..9619a3e6 100644 --- a/obd/OBDResponse.py +++ b/obd/OBDResponse.py @@ -89,22 +89,39 @@ def __str__(self): class Monitor(): def __init__(self): - self.tests = [] + self._tests = {} # tid : MonitorTest + + # make the standard TIDs available as null monitor tests + # until real data comes it. This also prevents things from + # breaking when the user looks up a standard test that's null. + null_test = MonitorTest() - # make all TID tests available as properties for tid in TEST_IDS: name = TEST_IDS[tid][0] - test = MonitorTest() - self.__dict__[name] = test - self.tests.append(test) + self.__dict__[name] = null_test + self._tests[tid] = null_test + + def add_test(self, test): + self._tests[test.tid] = test + if test.name is not None: + self.__dict__[test.name] = test + + @property + def tests(self): + return [test for test in self._tests.values() if not test.is_null()] def __str__(self): - valid_tests = [str(test) for test in self.tests if not test.is_null()] - if len(valid_tests) > 0: - return "\n".join(valid_tests) + if len(self.tests) > 0: + return "\n".join([ str(t) for t in self.tests ]) else: return "No tests to report" + def __len__(self): + return len(self.tests) + + def __getitem__(self, tid): + return self._tests.get(tid, MonitorTest()) + class MonitorTest(): def __init__(self): diff --git a/obd/__init__.py b/obd/__init__.py index 6781af6c..d95f5815 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -49,7 +49,7 @@ import logging logger = logging.getLogger(__name__) -logger.setLevel(logging.WARNING) +logger.setLevel(logging.DEBUG) console_handler = logging.StreamHandler() # sends output to stderr console_handler.setFormatter(logging.Formatter("[%(name)s] %(message)s")) diff --git a/obd/decoders.py b/obd/decoders.py index 3a376d4f..fcd9aca0 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -429,32 +429,33 @@ def dtc(messages): def parse_monitor_test(d, mon): + test = MonitorTest() + tid = d[1] - if tid not in TEST_IDS: + if tid in TEST_IDS: + test.name = TEST_IDS[tid][0] # lookup the name from the table + test.desc = TEST_IDS[tid][1] # lookup the description from the table + else: logger.debug("Encountered unknown Test ID") - return # if it's an unknown TID, abort - - name = TEST_IDS[tid][0] # lookup the name from the table - desc = TEST_IDS[tid][1] # lookup the description from the table - - test = mon.__dict__[name] # use the "name" field to lookup the right test + test.name = "Unknown" + test.desc = "Unknown" uas = UAS_IDS.get(d[2], None) - # if we can't decode the value, return a null MonitorTest + # if we can't decode the value, abort if uas is None: logger.debug("Encountered unknown Units and Scaling ID") - return + return None # load the test results test.tid = tid - test.name = name - test.desc = desc test.value = uas(d[3:5]) # convert bytes to actual values test.min = uas(d[5:7]) test.max = uas(d[7:]) + return test + def monitor(messages): d = messages[0].data @@ -469,6 +470,9 @@ def monitor(messages): # look at data in blocks of 9 bytes (one test result) for n in range(0, len(d), 9): - parse_monitor_test(d[n:n + 9], mon) # extract the 9 byte block, and parse a new MonitorTest + # extract the 9 byte block, and parse a new MonitorTest + test = parse_monitor_test(d[n:n + 9], mon) + if test is not None: + mon.add_test(test) return mon diff --git a/tests/test_decoders.py b/tests/test_decoders.py index f210f7ba..ae3e0a0b 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -200,7 +200,7 @@ def test_dtc(): ] def test_monitor(): - # v = d.monitor(m("01010A0BB00BB00BB00105100048000000640185240096004BFFFF")) - v = d.monitor(m("01010A0BB00BB00BB0")) + v = d.monitor(m("01010A0BB00BB00BB00105100048000000640185240096004BFFFF")) + # v = d.monitor(m("01010A0BB00BB00BB0")) print(v) assert(False) From 4a4a783fc0b563322cd3cb85e22b650accb703e2 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 16:57:23 -0400 Subject: [PATCH 378/569] wrote basic tests for monitor decoder --- obd/codes.py | 4 +-- tests/test_decoders.py | 63 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/obd/codes.py b/obd/codes.py index c453a139..69922c89 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -2209,8 +2209,8 @@ TEST_IDS = { # : # 0x0 is reserved - 0x01 : ("rtl_threshold voltage", "Rich to lean sensor threshold voltage"), - 0x02 : ("ltr_threshold voltage", "Lean to rich sensor threshold voltage"), + 0x01 : ("rtl_threshold_voltage", "Rich to lean sensor threshold voltage"), + 0x02 : ("ltr_threshold_voltage", "Lean to rich sensor threshold voltage"), 0x03 : ("low_voltage_switch_time", "Low sensor voltage for switch time calculation"), 0x04 : ("high_voltage_switch_time", "High sensor voltage for switch time calculation"), 0x05 : ("rtl_switch_time", "Rich to lean sensor switch time"), diff --git a/tests/test_decoders.py b/tests/test_decoders.py index ae3e0a0b..028b50f7 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -15,7 +15,7 @@ def m(hex_data, frames=[]): return [message] -FLOAT_EQUALS_TOLERANCE = 0.02 +FLOAT_EQUALS_TOLERANCE = 0.025 # comparison for pint floating point values def float_equals(va, vb): @@ -200,7 +200,62 @@ def test_dtc(): ] def test_monitor(): + # single test ----------------------------------------- + # [ test ] + v = d.monitor(m("01010A0BB00BB00BB0")) + assert len(v) == 1 # 1 test result + + # make sure we can look things up by name and TID + assert v[0x01] == v.rtl_threshold_voltage + + # make sure we got information + assert not v[0x01].is_null() + + assert float_equals(v[0x01].value, 365 * Unit.millivolt) + assert float_equals(v[0x01].min, 365 * Unit.millivolt) + assert float_equals(v[0x01].max, 365 * Unit.millivolt) + + # multiple tests -------------------------------------- + # [ test ][ test ][ test ] v = d.monitor(m("01010A0BB00BB00BB00105100048000000640185240096004BFFFF")) - # v = d.monitor(m("01010A0BB00BB00BB0")) - print(v) - assert(False) + assert len(v) == 3 # 3 test results + + # make sure we can look things up by name and TID + assert v[0x01] == v.rtl_threshold_voltage + assert v[0x05] == v.rtl_switch_time + + # make sure we got information + assert not v[0x01].is_null() + assert not v[0x05].is_null() + assert not v[0x85].is_null() + + assert float_equals(v[0x01].value, 365 * Unit.millivolt) + assert float_equals(v[0x01].min, 365 * Unit.millivolt) + assert float_equals(v[0x01].max, 365 * Unit.millivolt) + + assert float_equals(v[0x05].value, 72 * Unit.millisecond) + assert float_equals(v[0x05].min, 0 * Unit.millisecond) + assert float_equals(v[0x05].max, 100 * Unit.millisecond) + + assert float_equals(v[0x85].value, 150 * Unit.count) + assert float_equals(v[0x85].min, 75 * Unit.count) + assert float_equals(v[0x85].max, 65535 * Unit.count) + + # truncate incomplete tests ---------------------------- + # [ test ][junk] + v = d.monitor(m("01010A0BB00BB00BB0ABCDEF")) + assert len(v) == 1 # 1 test result + + # make sure we can look things up by name and TID + assert v[0x01] == v.rtl_threshold_voltage + + # make sure we got information + assert not v[0x01].is_null() + + assert float_equals(v[0x01].value, 365 * Unit.millivolt) + assert float_equals(v[0x01].min, 365 * Unit.millivolt) + assert float_equals(v[0x01].max, 365 * Unit.millivolt) + + # truncate incomplete tests ---------------------------- + v = d.monitor(m("01010A0BB00BB00B")) + assert len(v) == 0 # no valid tests From f90302d3786028fa57c5e5b7dff19bad02c0863c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 16:59:37 -0400 Subject: [PATCH 379/569] check that undefined tests are null --- tests/test_decoders.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 028b50f7..d6407d7a 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -3,6 +3,7 @@ from obd.UnitsAndScaling import Unit from obd.protocols.protocol import Frame, Message +from obd.codes import TEST_IDS import obd.decoders as d @@ -259,3 +260,8 @@ def test_monitor(): # truncate incomplete tests ---------------------------- v = d.monitor(m("01010A0BB00BB00B")) assert len(v) == 0 # no valid tests + + # make sure that the standard tests are null + for tid in TEST_IDS: + name = TEST_IDS[tid][0] + assert v[tid].is_null() From 26ef4623aba6cfd9daa93c8819648795cbfa6a3b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 17:15:59 -0400 Subject: [PATCH 380/569] wired up all mode 06 commands to the monitor decoder --- obd/commands.py | 176 ++++++++++++++++++++++++------------------------ 1 file changed, 88 insertions(+), 88 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 04739de0..27f666c1 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -183,104 +183,104 @@ # Mode 06 calls PID's MID's (Monitor ID) # This is for CAN only # name description cmd bytes decoder ECU fast - OBDCommand("MIDS_A" , "Supported MIDs [01-20]" , b"0600", 0, pid, ECU.ALL, False), - OBDCommand("MON_O2_B1S1" , "O2 Sensor Monitor Bank 1 - Sensor 1" , b"0601", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_B1S2" , "O2 Sensor Monitor Bank 1 - Sensor 2" , b"0602", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_B1S3" , "O2 Sensor Monitor Bank 1 - Sensor 3" , b"0603", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_B1S4" , "O2 Sensor Monitor Bank 1 - Sensor 4" , b"0604", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_B2S1" , "O2 Sensor Monitor Bank 2 - Sensor 1" , b"0605", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_B2S2" , "O2 Sensor Monitor Bank 2 - Sensor 2" , b"0606", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_B2S3" , "O2 Sensor Monitor Bank 2 - Sensor 3" , b"0607", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_B2S4" , "O2 Sensor Monitor Bank 2 - Sensor 4" , b"0608", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_B3S1" , "O2 Sensor Monitor Bank 3 - Sensor 1" , b"0609", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_B3S2" , "O2 Sensor Monitor Bank 3 - Sensor 2" , b"060A", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_B3S3" , "O2 Sensor Monitor Bank 3 - Sensor 3" , b"060B", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_B3S4" , "O2 Sensor Monitor Bank 3 - Sensor 4" , b"060C", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_B4S1" , "O2 Sensor Monitor Bank 4 - Sensor 1" , b"060D", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_B4S2" , "O2 Sensor Monitor Bank 4 - Sensor 2" , b"060E", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_B4S3" , "O2 Sensor Monitor Bank 4 - Sensor 3" , b"060F", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_B4S4" , "O2 Sensor Monitor Bank 4 - Sensor 4" , b"0610", 0, drop, ECU.ALL, False), + OBDCommand("MIDS_A" , "Supported MIDs [01-20]" , b"0600", 0, pid, ECU.ALL, False), + OBDCommand("MONITOR_O2_B1S1" , "O2 Sensor Monitor Bank 1 - Sensor 1" , b"0601", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_B1S2" , "O2 Sensor Monitor Bank 1 - Sensor 2" , b"0602", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_B1S3" , "O2 Sensor Monitor Bank 1 - Sensor 3" , b"0603", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_B1S4" , "O2 Sensor Monitor Bank 1 - Sensor 4" , b"0604", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_B2S1" , "O2 Sensor Monitor Bank 2 - Sensor 1" , b"0605", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_B2S2" , "O2 Sensor Monitor Bank 2 - Sensor 2" , b"0606", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_B2S3" , "O2 Sensor Monitor Bank 2 - Sensor 3" , b"0607", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_B2S4" , "O2 Sensor Monitor Bank 2 - Sensor 4" , b"0608", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_B3S1" , "O2 Sensor Monitor Bank 3 - Sensor 1" , b"0609", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_B3S2" , "O2 Sensor Monitor Bank 3 - Sensor 2" , b"060A", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_B3S3" , "O2 Sensor Monitor Bank 3 - Sensor 3" , b"060B", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_B3S4" , "O2 Sensor Monitor Bank 3 - Sensor 4" , b"060C", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_B4S1" , "O2 Sensor Monitor Bank 4 - Sensor 1" , b"060D", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_B4S2" , "O2 Sensor Monitor Bank 4 - Sensor 2" , b"060E", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_B4S3" , "O2 Sensor Monitor Bank 4 - Sensor 3" , b"060F", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_B4S4" , "O2 Sensor Monitor Bank 4 - Sensor 4" , b"0610", 0, monitor, ECU.ALL, False), ] + ([None] * 15) + [ # 11 - 1F Reserved - OBDCommand("MIDS_B" , "Supported MIDs [21-40]" , b"0620", 0, pid, ECU.ALL, False), - OBDCommand("MON_CATALYST_B1" , "Catalyst Monitor Bank 1" , b"0621", 0, drop, ECU.ALL, False), - OBDCommand("MON_CATALYST_B2" , "Catalyst Monitor Bank 2" , b"0622", 0, drop, ECU.ALL, False), - OBDCommand("MON_CATALYST_B3" , "Catalyst Monitor Bank 3" , b"0623", 0, drop, ECU.ALL, False), - OBDCommand("MON_CATALYST_B4" , "Catalyst Monitor Bank 4" , b"0624", 0, drop, ECU.ALL, False), + OBDCommand("MIDS_B" , "Supported MIDs [21-40]" , b"0620", 0, pid, ECU.ALL, False), + OBDCommand("MONITOR_CATALYST_B1" , "Catalyst Monitor Bank 1" , b"0621", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_CATALYST_B2" , "Catalyst Monitor Bank 2" , b"0622", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_CATALYST_B3" , "Catalyst Monitor Bank 3" , b"0623", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_CATALYST_B4" , "Catalyst Monitor Bank 4" , b"0624", 0, monitor, ECU.ALL, False), ] + ([None] * 12) + [ # 25 - 30 Reserved - OBDCommand("MON_EGR_B1" , "EGR Monitor Bank 1" , b"0631", 0, drop, ECU.ALL, False), - OBDCommand("MON_EGR_B2" , "EGR Monitor Bank 2" , b"0632", 0, drop, ECU.ALL, False), - OBDCommand("MON_EGR_B3" , "EGR Monitor Bank 3" , b"0633", 0, drop, ECU.ALL, False), - OBDCommand("MON_EGR_B4" , "EGR Monitor Bank 4" , b"0634", 0, drop, ECU.ALL, False), - OBDCommand("MON_VVT_B1" , "VVT Monitor Bank 1" , b"0635", 0, drop, ECU.ALL, False), - OBDCommand("MON_VVT_B2" , "VVT Monitor Bank 2" , b"0636", 0, drop, ECU.ALL, False), - OBDCommand("MON_VVT_B3" , "VVT Monitor Bank 3" , b"0637", 0, drop, ECU.ALL, False), - OBDCommand("MON_VVT_B4" , "VVT Monitor Bank 4" , b"0638", 0, drop, ECU.ALL, False), - OBDCommand("MON_EVAP_150" , "EVAP Monitor (Cap Off / 0.150\")" , b"0639", 0, drop, ECU.ALL, False), - OBDCommand("MON_EVAP_090" , "EVAP Monitor (0.090\")" , b"063A", 0, drop, ECU.ALL, False), - OBDCommand("MON_EVAP_040" , "EVAP Monitor (0.040\")" , b"063B", 0, drop, ECU.ALL, False), - OBDCommand("MON_EVAP_020" , "EVAP Monitor (0.020\")" , b"063C", 0, drop, ECU.ALL, False), - OBDCommand("MON_PURGE_FLOW" , "Purge Flow Monitor" , b"063D", 0, drop, ECU.ALL, False), + OBDCommand("MONITOR_EGR_B1" , "EGR Monitor Bank 1" , b"0631", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_EGR_B2" , "EGR Monitor Bank 2" , b"0632", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_EGR_B3" , "EGR Monitor Bank 3" , b"0633", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_EGR_B4" , "EGR Monitor Bank 4" , b"0634", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_VVT_B1" , "VVT Monitor Bank 1" , b"0635", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_VVT_B2" , "VVT Monitor Bank 2" , b"0636", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_VVT_B3" , "VVT Monitor Bank 3" , b"0637", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_VVT_B4" , "VVT Monitor Bank 4" , b"0638", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_EVAP_150" , "EVAP Monitor (Cap Off / 0.150\")" , b"0639", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_EVAP_090" , "EVAP Monitor (0.090\")" , b"063A", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_EVAP_040" , "EVAP Monitor (0.040\")" , b"063B", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_EVAP_020" , "EVAP Monitor (0.020\")" , b"063C", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_PURGE_FLOW" , "Purge Flow Monitor" , b"063D", 0, monitor, ECU.ALL, False), ] + ([None] * 2) + [ # 3E - 3F Reserved - OBDCommand("MIDS_C" , "Supported MIDs [41-60]" , b"0640", 0, pid, ECU.ALL, False), - OBDCommand("MON_O2_HEATER_B1S1" , "O2 Sensor Heater Monitor Bank 1 - Sensor 1" , b"0641", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_HEATER_B1S2" , "O2 Sensor Heater Monitor Bank 1 - Sensor 2" , b"0642", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_HEATER_B1S3" , "O2 Sensor Heater Monitor Bank 1 - Sensor 3" , b"0643", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_HEATER_B1S4" , "O2 Sensor Heater Monitor Bank 1 - Sensor 4" , b"0644", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_HEATER_B2S1" , "O2 Sensor Heater Monitor Bank 2 - Sensor 1" , b"0645", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_HEATER_B2S2" , "O2 Sensor Heater Monitor Bank 2 - Sensor 2" , b"0646", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_HEATER_B2S3" , "O2 Sensor Heater Monitor Bank 2 - Sensor 3" , b"0647", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_HEATER_B2S4" , "O2 Sensor Heater Monitor Bank 2 - Sensor 4" , b"0648", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_HEATER_B3S1" , "O2 Sensor Heater Monitor Bank 3 - Sensor 1" , b"0649", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_HEATER_B3S2" , "O2 Sensor Heater Monitor Bank 3 - Sensor 2" , b"064A", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_HEATER_B3S3" , "O2 Sensor Heater Monitor Bank 3 - Sensor 3" , b"064B", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_HEATER_B3S4" , "O2 Sensor Heater Monitor Bank 3 - Sensor 4" , b"064C", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_HEATER_B4S1" , "O2 Sensor Heater Monitor Bank 4 - Sensor 1" , b"064D", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_HEATER_B4S2" , "O2 Sensor Heater Monitor Bank 4 - Sensor 2" , b"064E", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_HEATER_B4S3" , "O2 Sensor Heater Monitor Bank 4 - Sensor 3" , b"064F", 0, drop, ECU.ALL, False), - OBDCommand("MON_O2_HEATER_B4S4" , "O2 Sensor Heater Monitor Bank 4 - Sensor 4" , b"0650", 0, drop, ECU.ALL, False), + OBDCommand("MIDS_C" , "Supported MIDs [41-60]" , b"0640", 0, pid, ECU.ALL, False), + OBDCommand("MONITOR_O2_HEATER_B1S1" , "O2 Sensor Heater Monitor Bank 1 - Sensor 1" , b"0641", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_HEATER_B1S2" , "O2 Sensor Heater Monitor Bank 1 - Sensor 2" , b"0642", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_HEATER_B1S3" , "O2 Sensor Heater Monitor Bank 1 - Sensor 3" , b"0643", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_HEATER_B1S4" , "O2 Sensor Heater Monitor Bank 1 - Sensor 4" , b"0644", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_HEATER_B2S1" , "O2 Sensor Heater Monitor Bank 2 - Sensor 1" , b"0645", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_HEATER_B2S2" , "O2 Sensor Heater Monitor Bank 2 - Sensor 2" , b"0646", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_HEATER_B2S3" , "O2 Sensor Heater Monitor Bank 2 - Sensor 3" , b"0647", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_HEATER_B2S4" , "O2 Sensor Heater Monitor Bank 2 - Sensor 4" , b"0648", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_HEATER_B3S1" , "O2 Sensor Heater Monitor Bank 3 - Sensor 1" , b"0649", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_HEATER_B3S2" , "O2 Sensor Heater Monitor Bank 3 - Sensor 2" , b"064A", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_HEATER_B3S3" , "O2 Sensor Heater Monitor Bank 3 - Sensor 3" , b"064B", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_HEATER_B3S4" , "O2 Sensor Heater Monitor Bank 3 - Sensor 4" , b"064C", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_HEATER_B4S1" , "O2 Sensor Heater Monitor Bank 4 - Sensor 1" , b"064D", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_HEATER_B4S2" , "O2 Sensor Heater Monitor Bank 4 - Sensor 2" , b"064E", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_HEATER_B4S3" , "O2 Sensor Heater Monitor Bank 4 - Sensor 3" , b"064F", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_O2_HEATER_B4S4" , "O2 Sensor Heater Monitor Bank 4 - Sensor 4" , b"0650", 0, monitor, ECU.ALL, False), ] + ([None] * 15) + [ # 51 - 5F Reserved - OBDCommand("MIDS_D" , "Supported MIDs [61-80]" , b"0660", 0, pid, ECU.ALL, False), - OBDCommand("MON_HEATED_CATALYST_B1" , "Heated Catalyst Monitor Bank 1" , b"0661", 0, drop, ECU.ALL, False), - OBDCommand("MON_HEATED_CATALYST_B2" , "Heated Catalyst Monitor Bank 2" , b"0662", 0, drop, ECU.ALL, False), - OBDCommand("MON_HEATED_CATALYST_B3" , "Heated Catalyst Monitor Bank 3" , b"0663", 0, drop, ECU.ALL, False), - OBDCommand("MON_HEATED_CATALYST_B4" , "Heated Catalyst Monitor Bank 4" , b"0664", 0, drop, ECU.ALL, False), + OBDCommand("MIDS_D" , "Supported MIDs [61-80]" , b"0660", 0, pid, ECU.ALL, False), + OBDCommand("MONITOR_HEATED_CATALYST_B1" , "Heated Catalyst Monitor Bank 1" , b"0661", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_HEATED_CATALYST_B2" , "Heated Catalyst Monitor Bank 2" , b"0662", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_HEATED_CATALYST_B3" , "Heated Catalyst Monitor Bank 3" , b"0663", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_HEATED_CATALYST_B4" , "Heated Catalyst Monitor Bank 4" , b"0664", 0, monitor, ECU.ALL, False), ] + ([None] * 12) + [ # 65 - 70 Reserved - OBDCommand("MON_SECONDARY_AIR_1" , "Secondary Air Monitor 1" , b"0671", 0, drop, ECU.ALL, False), - OBDCommand("MON_SECONDARY_AIR_2" , "Secondary Air Monitor 2" , b"0672", 0, drop, ECU.ALL, False), - OBDCommand("MON_SECONDARY_AIR_3" , "Secondary Air Monitor 3" , b"0673", 0, drop, ECU.ALL, False), - OBDCommand("MON_SECONDARY_AIR_4" , "Secondary Air Monitor 4" , b"0674", 0, drop, ECU.ALL, False), + OBDCommand("MONITOR_SECONDARY_AIR_1" , "Secondary Air Monitor 1" , b"0671", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_SECONDARY_AIR_2" , "Secondary Air Monitor 2" , b"0672", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_SECONDARY_AIR_3" , "Secondary Air Monitor 3" , b"0673", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_SECONDARY_AIR_4" , "Secondary Air Monitor 4" , b"0674", 0, monitor, ECU.ALL, False), ] + ([None] * 11) + [ # 75 - 7F Reserved - OBDCommand("MIDS_E" , "Supported MIDs [81-A0]" , b"0680", 0, pid, ECU.ALL, False), - OBDCommand("MON_FUEL_SYSTEM_B1" , "Fuel System Monitor Bank 1" , b"0681", 0, drop, ECU.ALL, False), - OBDCommand("MON_FUEL_SYSTEM_B2" , "Fuel System Monitor Bank 2" , b"0682", 0, drop, ECU.ALL, False), - OBDCommand("MON_FUEL_SYSTEM_B3" , "Fuel System Monitor Bank 3" , b"0683", 0, drop, ECU.ALL, False), - OBDCommand("MON_FUEL_SYSTEM_B4" , "Fuel System Monitor Bank 4" , b"0684", 0, drop, ECU.ALL, False), - OBDCommand("MON_BOOST_PRESSURE_B1" , "Boost Pressure Control Monitor Bank 1" , b"0685", 0, drop, ECU.ALL, False), - OBDCommand("MON_BOOST_PRESSURE_B2" , "Boost Pressure Control Monitor Bank 1" , b"0686", 0, drop, ECU.ALL, False), + OBDCommand("MIDS_E" , "Supported MIDs [81-A0]" , b"0680", 0, pid, ECU.ALL, False), + OBDCommand("MONITOR_FUEL_SYSTEM_B1" , "Fuel System Monitor Bank 1" , b"0681", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_FUEL_SYSTEM_B2" , "Fuel System Monitor Bank 2" , b"0682", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_FUEL_SYSTEM_B3" , "Fuel System Monitor Bank 3" , b"0683", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_FUEL_SYSTEM_B4" , "Fuel System Monitor Bank 4" , b"0684", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_BOOST_PRESSURE_B1" , "Boost Pressure Control Monitor Bank 1" , b"0685", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_BOOST_PRESSURE_B2" , "Boost Pressure Control Monitor Bank 1" , b"0686", 0, monitor, ECU.ALL, False), ] + ([None] * 9) + [ # 87 - 8F Reserved - OBDCommand("MON_NOX_ABSORBER_B1" , "NOx Absorber Monitor Bank 1" , b"0690", 0, drop, ECU.ALL, False), - OBDCommand("MON_NOX_ABSORBER_B2" , "NOx Absorber Monitor Bank 2" , b"0691", 0, drop, ECU.ALL, False), + OBDCommand("MONITOR_NOX_ABSORBER_B1" , "NOx Absorber Monitor Bank 1" , b"0690", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_NOX_ABSORBER_B2" , "NOx Absorber Monitor Bank 2" , b"0691", 0, monitor, ECU.ALL, False), ] + ([None] * 6) + [ # 92 - 97 Reserved - OBDCommand("MON_NOX_CATALYST_B1" , "NOx Catalyst Monitor Bank 1" , b"0698", 0, drop, ECU.ALL, False), - OBDCommand("MON_NOX_CATALYST_B2" , "NOx Catalyst Monitor Bank 2" , b"0699", 0, drop, ECU.ALL, False), + OBDCommand("MONITOR_NOX_CATALYST_B1" , "NOx Catalyst Monitor Bank 1" , b"0698", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_NOX_CATALYST_B2" , "NOx Catalyst Monitor Bank 2" , b"0699", 0, monitor, ECU.ALL, False), ] + ([None] * 6) + [ # 9A - 9F Reserved - OBDCommand("MIDS_F" , "Supported MIDs [A1-C0]" , b"06A0", 0, pid, ECU.ALL, False), - OBDCommand("MON_MISFIRE_GENERAL" , "Misfire Monitor General Data" , b"06A1", 0, drop, ECU.ALL, False), - OBDCommand("MON_MISFIRE_CYLINDER_1" , "Misfire Cylinder 1 Data" , b"06A2", 0, drop, ECU.ALL, False), - OBDCommand("MON_MISFIRE_CYLINDER_2" , "Misfire Cylinder 2 Data" , b"06A3", 0, drop, ECU.ALL, False), - OBDCommand("MON_MISFIRE_CYLINDER_3" , "Misfire Cylinder 3 Data" , b"06A4", 0, drop, ECU.ALL, False), - OBDCommand("MON_MISFIRE_CYLINDER_4" , "Misfire Cylinder 4 Data" , b"06A5", 0, drop, ECU.ALL, False), - OBDCommand("MON_MISFIRE_CYLINDER_5" , "Misfire Cylinder 5 Data" , b"06A6", 0, drop, ECU.ALL, False), - OBDCommand("MON_MISFIRE_CYLINDER_6" , "Misfire Cylinder 6 Data" , b"06A7", 0, drop, ECU.ALL, False), - OBDCommand("MON_MISFIRE_CYLINDER_7" , "Misfire Cylinder 7 Data" , b"06A8", 0, drop, ECU.ALL, False), - OBDCommand("MON_MISFIRE_CYLINDER_8" , "Misfire Cylinder 8 Data" , b"06A9", 0, drop, ECU.ALL, False), - OBDCommand("MON_MISFIRE_CYLINDER_9" , "Misfire Cylinder 9 Data" , b"06AA", 0, drop, ECU.ALL, False), - OBDCommand("MON_MISFIRE_CYLINDER_10" , "Misfire Cylinder 10 Data" , b"06AB", 0, drop, ECU.ALL, False), - OBDCommand("MON_MISFIRE_CYLINDER_11" , "Misfire Cylinder 11 Data" , b"06AC", 0, drop, ECU.ALL, False), - OBDCommand("MON_MISFIRE_CYLINDER_12" , "Misfire Cylinder 12 Data" , b"06AD", 0, drop, ECU.ALL, False), + OBDCommand("MIDS_F" , "Supported MIDs [A1-C0]" , b"06A0", 0, pid, ECU.ALL, False), + OBDCommand("MONITOR_MISFIRE_GENERAL" , "Misfire Monitor General Data" , b"06A1", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_MISFIRE_CYLINDER_1" , "Misfire Cylinder 1 Data" , b"06A2", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_MISFIRE_CYLINDER_2" , "Misfire Cylinder 2 Data" , b"06A3", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_MISFIRE_CYLINDER_3" , "Misfire Cylinder 3 Data" , b"06A4", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_MISFIRE_CYLINDER_4" , "Misfire Cylinder 4 Data" , b"06A5", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_MISFIRE_CYLINDER_5" , "Misfire Cylinder 5 Data" , b"06A6", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_MISFIRE_CYLINDER_6" , "Misfire Cylinder 6 Data" , b"06A7", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_MISFIRE_CYLINDER_7" , "Misfire Cylinder 7 Data" , b"06A8", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_MISFIRE_CYLINDER_8" , "Misfire Cylinder 8 Data" , b"06A9", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_MISFIRE_CYLINDER_9" , "Misfire Cylinder 9 Data" , b"06AA", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_MISFIRE_CYLINDER_10" , "Misfire Cylinder 10 Data" , b"06AB", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_MISFIRE_CYLINDER_11" , "Misfire Cylinder 11 Data" , b"06AC", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_MISFIRE_CYLINDER_12" , "Misfire Cylinder 12 Data" , b"06AD", 0, monitor, ECU.ALL, False), ] + ([None] * 2) + [ # AE - AF Reserved - OBDCommand("MON_PM_FILTER_B1" , "PM Filter Monitor Bank 1" , b"06B0", 0, drop, ECU.ALL, False), - OBDCommand("MON_PM_FILTER_B2" , "PM Filter Monitor Bank 2" , b"06B1", 0, drop, ECU.ALL, False), + OBDCommand("MONITOR_PM_FILTER_B1" , "PM Filter Monitor Bank 1" , b"06B0", 0, monitor, ECU.ALL, False), + OBDCommand("MONITOR_PM_FILTER_B2" , "PM Filter Monitor Bank 2" , b"06B1", 0, monitor, ECU.ALL, False), ] __mode7__ = [ From 00359ca31487a53cecc497d5d75028254eccc2b4 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 17:48:18 -0400 Subject: [PATCH 381/569] added test for special handling of mode 06 in CAN --- tests/test_protocol_can.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index cfa10789..4392898f 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -201,6 +201,27 @@ def test_multi_line_mode_03(): check_message(r[0], len(test_case), 0, correct_data) +def test_multi_line_mode_06(): + """ + Tests the special handling of mode 6 commands. + The parser should chop off only the Mode byte from the response. + """ + + for protocol in CAN_11_PROTOCOLS: + p = protocol([]) + + test_case = [ + "7E8 10 0A 46 01 01 0A 0B B0", + "7E8 21 0B B0 0B B0", + ] + + correct_data = [0x01, 0x01, 0x0A, 0x0B, 0xB0, 0x0B, 0xB0, 0x0B, 0xB0] + + r = p(test_case) + assert len(r) == 1 + check_message(r[0], len(test_case), 0, correct_data) + + def test_can_29(): pass From 2bc60d69dfa991fbe069bd891d22ec187473cc92 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 21:07:25 -0400 Subject: [PATCH 382/569] decided the old groups (by PID GET) was better --- obd/commands.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 27f666c1..16ac1347 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -66,8 +66,6 @@ OBDCommand("SPEED" , "Vehicle Speed" , b"010D", 1, speed, ECU.ENGINE, True), OBDCommand("TIMING_ADVANCE" , "Timing Advance" , b"010E", 1, timing_advance, ECU.ENGINE, True), OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , b"010F", 1, temp, ECU.ENGINE, True), - - # name description cmd bytes decoder ECU fast OBDCommand("MAF" , "Air Flow Rate (MAF)" , b"0110", 2, maf, ECU.ENGINE, True), OBDCommand("THROTTLE_POS" , "Throttle Position" , b"0111", 1, percent, ECU.ENGINE, True), OBDCommand("AIR_STATUS" , "Secondary Air Status" , b"0112", 1, air_status, ECU.ENGINE, True), @@ -102,8 +100,6 @@ OBDCommand("EGR_ERROR" , "EGR Error" , b"012D", 1, percent_centered, ECU.ENGINE, True), OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , b"012E", 1, percent, ECU.ENGINE, True), OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , b"012F", 1, percent, ECU.ENGINE, True), - - # name description cmd bytes decoder ECU fast OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , b"0130", 1, count, ECU.ENGINE, True), OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , b"0131", 2, distance, ECU.ENGINE, True), OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , b"0132", 2, evap_pressure, ECU.ENGINE, True), @@ -138,8 +134,6 @@ OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , b"014D", 2, minutes, ECU.ENGINE, True), OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , b"014E", 2, minutes, ECU.ENGINE, True), OBDCommand("MAX_VALUES" , "Various Max values" , b"014F", 4, drop, ECU.ENGINE, True), # todo: decode this - - # name description cmd bytes decoder ECU fast OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , b"0150", 4, max_maf, ECU.ENGINE, True), OBDCommand("FUEL_TYPE" , "Fuel Type" , b"0151", 1, fuel_type, ECU.ENGINE, True), OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , b"0152", 1, percent, ECU.ENGINE, True), From 9d104ef9bbb2935a21d5b7b8b7f1e0377a081fe1 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 21:13:27 -0400 Subject: [PATCH 383/569] use existing DTC decoder to report the freeze DTC --- obd/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/commands.py b/obd/commands.py index 16ac1347..dd068154 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -52,7 +52,7 @@ # name description cmd bytes decoder ECU fast OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , b"0100", 4, pid, ECU.ENGINE, True), OBDCommand("STATUS" , "Status since DTCs cleared" , b"0101", 4, status, ECU.ENGINE, True), - OBDCommand("FREEZE_DTC" , "Freeze DTC" , b"0102", 2, drop, ECU.ENGINE, True), + OBDCommand("FREEZE_DTC" , "Freeze DTC" , b"0102", 2, single_dtc, ECU.ENGINE, True), OBDCommand("FUEL_STATUS" , "Fuel System Status" , b"0103", 2, fuel_status, ECU.ENGINE, True), OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , b"0104", 1, percent, ECU.ENGINE, True), OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , b"0105", 1, temp, ECU.ENGINE, True), From 0af16eaf3f5f7a39740e38d8d8e72d75d1ef6e35 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 21:19:07 -0400 Subject: [PATCH 384/569] removed unsupported warning from docs for FREEZE_DTC --- docs/Commands.md | 2 +- obd/commands.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Commands.md b/docs/Commands.md index ab670aee..0e8ce600 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -76,7 +76,7 @@ obd.commands.has_pid(1, 12) # True |----|---------------------------|-----------------------------------------| | 00 | PIDS_A | Supported PIDs [01-20] | | 01 | STATUS | Status since DTCs cleared | -| 02 | *unsupported* | *unsupported* | +| 02 | FREEZE_DTC | DTC that triggered the freeze frame | | 03 | FUEL_STATUS | Fuel System Status | | 04 | ENGINE_LOAD | Calculated Engine Load | | 05 | COOLANT_TEMP | Engine Coolant Temperature | diff --git a/obd/commands.py b/obd/commands.py index dd068154..f4595e35 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -52,7 +52,7 @@ # name description cmd bytes decoder ECU fast OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , b"0100", 4, pid, ECU.ENGINE, True), OBDCommand("STATUS" , "Status since DTCs cleared" , b"0101", 4, status, ECU.ENGINE, True), - OBDCommand("FREEZE_DTC" , "Freeze DTC" , b"0102", 2, single_dtc, ECU.ENGINE, True), + OBDCommand("FREEZE_DTC" , "DTC that triggered the freeze frame" , b"0102", 2, single_dtc, ECU.ENGINE, True), OBDCommand("FUEL_STATUS" , "Fuel System Status" , b"0103", 2, fuel_status, ECU.ENGINE, True), OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , b"0104", 1, percent, ECU.ENGINE, True), OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , b"0105", 1, temp, ECU.ENGINE, True), From 7adcd1ae3b18e0fb72f5f9853629c70a540566e6 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 21:40:10 -0400 Subject: [PATCH 385/569] implemented bit-encoded O2 sensor decoders --- obd/commands.py | 4 ++-- obd/decoders.py | 19 +++++++++++++++++++ tests/test_decoders.py | 12 ++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index f4595e35..333297fe 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -69,7 +69,7 @@ OBDCommand("MAF" , "Air Flow Rate (MAF)" , b"0110", 2, maf, ECU.ENGINE, True), OBDCommand("THROTTLE_POS" , "Throttle Position" , b"0111", 1, percent, ECU.ENGINE, True), OBDCommand("AIR_STATUS" , "Secondary Air Status" , b"0112", 1, air_status, ECU.ENGINE, True), - OBDCommand("O2_SENSORS" , "O2 Sensors Present" , b"0113", 1, drop, ECU.ENGINE, True), + OBDCommand("O2_SENSORS" , "O2 Sensors Present" , b"0113", 1, o2_sensors, ECU.ENGINE, True), OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , b"0114", 2, sensor_voltage, ECU.ENGINE, True), OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , b"0115", 2, sensor_voltage, ECU.ENGINE, True), OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , b"0116", 2, sensor_voltage, ECU.ENGINE, True), @@ -79,7 +79,7 @@ OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , b"011A", 2, sensor_voltage, ECU.ENGINE, True), OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , b"011B", 2, sensor_voltage, ECU.ENGINE, True), OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , b"011C", 1, obd_compliance, ECU.ENGINE, True), - OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , b"011D", 1, drop, ECU.ENGINE, True), + OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , b"011D", 1, o2_sensors_alt, ECU.ENGINE, True), OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , b"011E", 1, drop, ECU.ENGINE, True), OBDCommand("RUN_TIME" , "Engine Run Time" , b"011F", 2, seconds, ECU.ENGINE, True), diff --git a/obd/decoders.py b/obd/decoders.py index fcd9aca0..c91ba2d5 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -244,6 +244,25 @@ def fuel_rate(messages): v = v * 0.05 return v * Unit.liters_per_hour +# special bit encoding for PID 13 +def o2_sensors(messages): + d = messages[0].data + bitstring = bytes_to_bits(d) + return ( + tuple([ b == "1" for b in bitstring[:4] ]), # bank 1 + tuple([ b == "1" for b in bitstring[4:] ]), # bank 2 + ) + +# special bit encoding for PID 1D +def o2_sensors_alt(messages): + d = messages[0].data + bitstring = bytes_to_bits(d) + return ( + tuple([ b == "1" for b in bitstring[:2] ]), # bank 1 + tuple([ b == "1" for b in bitstring[2:4] ]), # bank 2 + tuple([ b == "1" for b in bitstring[4:6] ]), # bank 3 + tuple([ b == "1" for b in bitstring[6:] ]), # bank 4 + ) def elm_voltage(messages): # doesn't register as a normal OBD response, diff --git a/tests/test_decoders.py b/tests/test_decoders.py index d6407d7a..07d3a944 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -165,6 +165,18 @@ def test_air_status(): assert d.air_status(m("08")) == "Pump commanded on for diagnostics" assert d.air_status(m("03")) == None +def test_o2_sensors(): + assert d.o2_sensors(m("00")) == ((False, False, False, False), (False, False, False, False)) + assert d.o2_sensors(m("01")) == ((False, False, False, False), (False, False, False, True)) + assert d.o2_sensors(m("0F")) == ((False, False, False, False), (True, True, True, True)) + assert d.o2_sensors(m("F0")) == ((True, True, True, True), (False, False, False, False)) + +def test_o2_sensors_alt(): + assert d.o2_sensors_alt(m("00")) == ((False, False), (False, False), (False, False), (False, False)) + assert d.o2_sensors_alt(m("01")) == ((False, False), (False, False), (False, False), (False, True)) + assert d.o2_sensors_alt(m("0F")) == ((False, False), (False, False), (True, True), (True, True)) + assert d.o2_sensors_alt(m("F0")) == ((True, True), (True, True), (False, False), (False, False)) + def test_elm_voltage(): # these aren't parsed as standard hex messages, so manufacture our own assert d.elm_voltage([ Message([ Frame("12.875") ]) ]) == 12.875 * Unit.volt From e7725ca22f086f9a92675f70a5fe59a08c65206f Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 21:42:49 -0400 Subject: [PATCH 386/569] updated docs --- docs/Commands.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Commands.md b/docs/Commands.md index 0e8ce600..07fd4f47 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -93,7 +93,7 @@ obd.commands.has_pid(1, 12) # True | 10 | MAF | Air Flow Rate (MAF) | | 11 | THROTTLE_POS | Throttle Position | | 12 | AIR_STATUS | Secondary Air Status | -| 13 | *unsupported* | *unsupported* | +| 13 | O2_SENSORS | O2 Sensors Present | | 14 | O2_B1S1 | O2: Bank 1 - Sensor 1 Voltage | | 15 | O2_B1S2 | O2: Bank 1 - Sensor 2 Voltage | | 16 | O2_B1S3 | O2: Bank 1 - Sensor 3 Voltage | @@ -103,7 +103,7 @@ obd.commands.has_pid(1, 12) # True | 1A | O2_B2S3 | O2: Bank 2 - Sensor 3 Voltage | | 1B | O2_B2S4 | O2: Bank 2 - Sensor 4 Voltage | | 1C | OBD_COMPLIANCE | OBD Standards Compliance | -| 1D | *unsupported* | *unsupported* | +| 1D | O2_SENSORS_ALT | O2 Sensors Present (alternate) | | 1E | *unsupported* | *unsupported* | | 1F | RUN_TIME | Engine Run Time | | 20 | PIDS_B | Supported PIDs [21-40] | From 697c12678ae6e4d2f7deace2ed3274e3f2d87262 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 21:56:37 -0400 Subject: [PATCH 387/569] implemented aux input status --- obd/commands.py | 2 +- obd/decoders.py | 4 ++++ tests/test_decoders.py | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/obd/commands.py b/obd/commands.py index 333297fe..b12e4cfb 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -80,7 +80,7 @@ OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , b"011B", 2, sensor_voltage, ECU.ENGINE, True), OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , b"011C", 1, obd_compliance, ECU.ENGINE, True), OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , b"011D", 1, o2_sensors_alt, ECU.ENGINE, True), - OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , b"011E", 1, drop, ECU.ENGINE, True), + OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , b"011E", 1, aux_input_status, ECU.ENGINE, True), OBDCommand("RUN_TIME" , "Engine Run Time" , b"011F", 2, seconds, ECU.ENGINE, True), # name description cmd bytes decoder ECU fast diff --git a/obd/decoders.py b/obd/decoders.py index c91ba2d5..2a9e9447 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -253,6 +253,10 @@ def o2_sensors(messages): tuple([ b == "1" for b in bitstring[4:] ]), # bank 2 ) +def aux_input_status(messages): + d = messages[0].data + return ((d[0] >> 7) & 1) == 1 # first bit indicate PTO status + # special bit encoding for PID 1D def o2_sensors_alt(messages): d = messages[0].data diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 07d3a944..cd6bdb4a 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -177,6 +177,10 @@ def test_o2_sensors_alt(): assert d.o2_sensors_alt(m("0F")) == ((False, False), (False, False), (True, True), (True, True)) assert d.o2_sensors_alt(m("F0")) == ((True, True), (True, True), (False, False), (False, False)) +def test_aux_input_status(): + assert d.aux_input_status(m("00")) == False + assert d.aux_input_status(m("80")) == True + def test_elm_voltage(): # these aren't parsed as standard hex messages, so manufacture our own assert d.elm_voltage([ Message([ Frame("12.875") ]) ]) == 12.875 * Unit.volt From 1d8f5f0727c49fb9147a1a2a43b31b615f172390 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 30 Jun 2016 21:59:07 -0400 Subject: [PATCH 388/569] updated docs --- docs/Commands.md | 2 +- obd/commands.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Commands.md b/docs/Commands.md index 07fd4f47..8d422bf3 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -104,7 +104,7 @@ obd.commands.has_pid(1, 12) # True | 1B | O2_B2S4 | O2: Bank 2 - Sensor 4 Voltage | | 1C | OBD_COMPLIANCE | OBD Standards Compliance | | 1D | O2_SENSORS_ALT | O2 Sensors Present (alternate) | -| 1E | *unsupported* | *unsupported* | +| 1E | AUX_INPUT_STATUS | Auxiliary input status (power take off) | | 1F | RUN_TIME | Engine Run Time | | 20 | PIDS_B | Supported PIDs [21-40] | | 21 | DISTANCE_W_MIL | Distance Traveled with MIL on | diff --git a/obd/commands.py b/obd/commands.py index b12e4cfb..19860fda 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -80,7 +80,7 @@ OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , b"011B", 2, sensor_voltage, ECU.ENGINE, True), OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , b"011C", 1, obd_compliance, ECU.ENGINE, True), OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , b"011D", 1, o2_sensors_alt, ECU.ENGINE, True), - OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , b"011E", 1, aux_input_status, ECU.ENGINE, True), + OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status (power take off)" , b"011E", 1, aux_input_status, ECU.ENGINE, True), OBDCommand("RUN_TIME" , "Engine Run Time" , b"011F", 2, seconds, ECU.ENGINE, True), # name description cmd bytes decoder ECU fast From ec048adb2d07ed730cbc05387c1f44503a99aaf6 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 1 Jul 2016 00:56:38 -0400 Subject: [PATCH 389/569] removed old bitstring util --- obd/utils.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/obd/utils.py b/obd/utils.py index 015bc9bb..bdf57b51 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -83,12 +83,6 @@ def bytes_to_hex(bs): h += ("0" * (2 - len(bh))) + bh return h -def bitstring(_hex, bits=None): - b = bin(unhex(_hex))[2:] - if bits is not None: - b = ('0' * (bits - len(b))) + b - return b - def bitToBool(_bit): return (_bit == '1') From 3fa53d0e5d4057be2b8df6cc532d400e3ec74f9f Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 13:14:45 -0400 Subject: [PATCH 390/569] removed old unhex util --- obd/OBDCommand.py | 4 ++-- obd/decoders.py | 4 ++-- obd/utils.py | 4 ---- tests/test_protocol.py | 9 ++++----- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index c007816a..1426997c 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -67,14 +67,14 @@ def clone(self): @property def mode(self): if len(self.command) >= 2: - return unhex(self.command[:2]) + return int(self.command[:2], 16) else: return 0 @property def pid(self): if len(self.command) > 2: - return unhex(self.command[2:]) + return int(self.command[2:], 16) else: return 0 diff --git a/obd/decoders.py b/obd/decoders.py index 2a9e9447..8442f30e 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -160,8 +160,8 @@ def fuel_pres_direct(messages): def evap_pressure(messages): # decode the twos complement d = messages[0].data - a = twos_comp(unhex(d[0]), 8) - b = twos_comp(unhex(d[1]), 8) + a = twos_comp(d[0], 8) + b = twos_comp(d[1], 8) v = ((a * 256.0) + b) / 4.0 return v * Unit.pascal diff --git a/obd/utils.py b/obd/utils.py index bdf57b51..08ed6900 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -53,10 +53,6 @@ class OBDStatus: def num_bits_set(n): return bin(n).count("1") -def unhex(_hex): - _hex = "0" if _hex == "" else _hex - return int(_hex, 16) - def unbin(_bin): return int(_bin, 2) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index a7f95a1a..b623d58b 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -1,6 +1,5 @@ import random -from obd.utils import unhex from obd.protocols import * from obd.protocols.protocol import Frame, Message @@ -55,10 +54,10 @@ def test_message_hex(): message.data = b'\x00\x01\x02' assert message.hex() == b'000102' - assert unhex(message.hex()[0:2]) == 0x00 - assert unhex(message.hex()[2:4]) == 0x01 - assert unhex(message.hex()[4:6]) == 0x02 - assert unhex(message.hex()) == 0x000102 + assert int(message.hex()[0:2], 16) == 0x00 + assert int(message.hex()[2:4], 16) == 0x01 + assert int(message.hex()[4:6], 16) == 0x02 + assert int(message.hex(), 16) == 0x000102 def test_populate_ecu_map(): From be9bd8f012d4d7e82855c8c3f3e50709f9af80bc Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 13:24:57 -0400 Subject: [PATCH 391/569] fixed case where all correctly sized messages were running the padding routine --- obd/OBDCommand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 1426997c..3b064856 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -112,7 +112,7 @@ def __constrain_message_data(self, message): # chop off the right side message.data = message.data[:self.bytes] logger.debug("Message was longer than expected. Trimmed message: " + repr(message.data)) - else: + elif len(message.data) < self.bytes: # pad the right with zeros message.data += (b'\x00' * (self.bytes - len(message.data))) logger.debug("Message was shorter than expected. Padded message: " + repr(message.data)) From a323ad44e3ab2e846250069e7f14a9acfc37c611 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 14:13:27 -0400 Subject: [PATCH 392/569] replaced as many decoders as possible with calls to the UAS table --- obd/UnitsAndScaling.py | 2 +- obd/commands.py | 34 +++++++++--------- obd/decoders.py | 79 +++++++----------------------------------- 3 files changed, 31 insertions(+), 84 deletions(-) diff --git a/obd/UnitsAndScaling.py b/obd/UnitsAndScaling.py index f80cb701..f36fccb7 100644 --- a/obd/UnitsAndScaling.py +++ b/obd/UnitsAndScaling.py @@ -107,7 +107,7 @@ def __call__(self, _bytes): 0x24 : UAS(False, 1, Unit.count), 0x25 : UAS(False, 1, Unit.kilometer), 0x26 : UAS(False, 0.1, Unit.millivolt / Unit.millisecond), - 0x27 : UAS(False, 0.1, Unit.grams_per_second), + 0x27 : UAS(False, 0.01, Unit.grams_per_second), 0x28 : UAS(False, 1, Unit.grams_per_second), 0x29 : UAS(False, 0.25, Unit.pascal / Unit.second), 0x2A : UAS(False, 0.001, Unit.kilogram / Unit.hour), diff --git a/obd/commands.py b/obd/commands.py index 19860fda..d23bab5c 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -62,11 +62,11 @@ OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , b"0109", 1, percent_centered, ECU.ENGINE, True), OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , b"010A", 1, fuel_pressure, ECU.ENGINE, True), OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , b"010B", 1, pressure, ECU.ENGINE, True), - OBDCommand("RPM" , "Engine RPM" , b"010C", 2, rpm, ECU.ENGINE, True), - OBDCommand("SPEED" , "Vehicle Speed" , b"010D", 1, speed, ECU.ENGINE, True), + OBDCommand("RPM" , "Engine RPM" , b"010C", 2, uas(0x07), ECU.ENGINE, True), + OBDCommand("SPEED" , "Vehicle Speed" , b"010D", 1, uas(0x09), ECU.ENGINE, True), OBDCommand("TIMING_ADVANCE" , "Timing Advance" , b"010E", 1, timing_advance, ECU.ENGINE, True), OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , b"010F", 1, temp, ECU.ENGINE, True), - OBDCommand("MAF" , "Air Flow Rate (MAF)" , b"0110", 2, maf, ECU.ENGINE, True), + OBDCommand("MAF" , "Air Flow Rate (MAF)" , b"0110", 2, uas(0x27), ECU.ENGINE, True), OBDCommand("THROTTLE_POS" , "Throttle Position" , b"0111", 1, percent, ECU.ENGINE, True), OBDCommand("AIR_STATUS" , "Secondary Air Status" , b"0112", 1, air_status, ECU.ENGINE, True), OBDCommand("O2_SENSORS" , "O2 Sensors Present" , b"0113", 1, o2_sensors, ECU.ENGINE, True), @@ -81,13 +81,13 @@ OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , b"011C", 1, obd_compliance, ECU.ENGINE, True), OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , b"011D", 1, o2_sensors_alt, ECU.ENGINE, True), OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status (power take off)" , b"011E", 1, aux_input_status, ECU.ENGINE, True), - OBDCommand("RUN_TIME" , "Engine Run Time" , b"011F", 2, seconds, ECU.ENGINE, True), + OBDCommand("RUN_TIME" , "Engine Run Time" , b"011F", 2, uas(0x12), ECU.ENGINE, True), # name description cmd bytes decoder ECU fast OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , b"0120", 4, pid, ECU.ENGINE, True), - OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , b"0121", 2, distance, ECU.ENGINE, True), - OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , b"0122", 2, fuel_pres_vac, ECU.ENGINE, True), - OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , b"0123", 2, fuel_pres_direct, ECU.ENGINE, True), + OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , b"0121", 2, uas(0x25), ECU.ENGINE, True), + OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , b"0122", 2, uas(0x19), ECU.ENGINE, True), + OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , b"0123", 2, uas(0x1B), ECU.ENGINE, True), OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , b"0124", 4, sensor_voltage_big, ECU.ENGINE, True), OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , b"0125", 4, sensor_voltage_big, ECU.ENGINE, True), OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , b"0126", 4, sensor_voltage_big, ECU.ENGINE, True), @@ -100,8 +100,8 @@ OBDCommand("EGR_ERROR" , "EGR Error" , b"012D", 1, percent_centered, ECU.ENGINE, True), OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , b"012E", 1, percent, ECU.ENGINE, True), OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , b"012F", 1, percent, ECU.ENGINE, True), - OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , b"0130", 1, count, ECU.ENGINE, True), - OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , b"0131", 2, distance, ECU.ENGINE, True), + OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , b"0130", 1, uas(0x01), ECU.ENGINE, True), + OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , b"0131", 2, uas(0x25), ECU.ENGINE, True), OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , b"0132", 2, evap_pressure, ECU.ENGINE, True), OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , b"0133", 1, pressure, ECU.ENGINE, True), OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , b"0134", 4, current_centered, ECU.ENGINE, True), @@ -112,10 +112,10 @@ OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , b"0139", 4, current_centered, ECU.ENGINE, True), OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , b"013A", 4, current_centered, ECU.ENGINE, True), OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , b"013B", 4, current_centered, ECU.ENGINE, True), - OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , b"013C", 2, catalyst_temp, ECU.ENGINE, True), - OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , b"013D", 2, catalyst_temp, ECU.ENGINE, True), - OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , b"013E", 2, catalyst_temp, ECU.ENGINE, True), - OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , b"013F", 2, catalyst_temp, ECU.ENGINE, True), + OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , b"013C", 2, uas(0x16), ECU.ENGINE, True), + OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , b"013D", 2, uas(0x16), ECU.ENGINE, True), + OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , b"013E", 2, uas(0x16), ECU.ENGINE, True), + OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , b"013F", 2, uas(0x16), ECU.ENGINE, True), # name description cmd bytes decoder ECU fast OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , b"0140", 4, pid, ECU.ENGINE, True), @@ -131,8 +131,8 @@ OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , b"014A", 1, percent, ECU.ENGINE, True), OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , b"014B", 1, percent, ECU.ENGINE, True), OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , b"014C", 1, percent, ECU.ENGINE, True), - OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , b"014D", 2, minutes, ECU.ENGINE, True), - OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , b"014E", 2, minutes, ECU.ENGINE, True), + OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , b"014D", 2, uas(0x34), ECU.ENGINE, True), + OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , b"014E", 2, uas(0x34), ECU.ENGINE, True), OBDCommand("MAX_VALUES" , "Various Max values" , b"014F", 4, drop, ECU.ENGINE, True), # todo: decode this OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , b"0150", 4, max_maf, ECU.ENGINE, True), OBDCommand("FUEL_TYPE" , "Fuel Type" , b"0151", 1, fuel_type, ECU.ENGINE, True), @@ -143,7 +143,7 @@ OBDCommand("LONG_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , b"0156", 2, percent_centered, ECU.ENGINE, True), OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , b"0157", 2, percent_centered, ECU.ENGINE, True), OBDCommand("LONG_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , b"0158", 2, percent_centered, ECU.ENGINE, True), - OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , b"0159", 2, fuel_pres_direct, ECU.ENGINE, True), + OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , b"0159", 2, uas(0x1B), ECU.ENGINE, True), OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , b"015A", 1, percent, ECU.ENGINE, True), OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , b"015B", 1, percent, ECU.ENGINE, True), OBDCommand("OIL_TEMP" , "Engine oil temperature" , b"015C", 1, temp, ECU.ENGINE, True), @@ -285,7 +285,7 @@ __mode9__ = [ # name description cmd bytes decoder ECU fast OBDCommand("PIDS_9A" , "Supported PIDs [01-20]" , b"0900", 4, pid, ECU.ENGINE, True), - OBDCommand("VIN_MESSAGE_COUNT" , "VIN Message Count" , b"0901", 1, count, ECU.ENGINE, True), + OBDCommand("VIN_MESSAGE_COUNT" , "VIN Message Count" , b"0901", 1, uas(0x01), ECU.ENGINE, True), OBDCommand("VIN" , "Get Vehicle Identification Number" , b"0902", 20, raw_string, ECU.ENGINE, True), ] diff --git a/obd/decoders.py b/obd/decoders.py index 8442f30e..55470b0c 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -30,6 +30,7 @@ ######################################################################## import math +import functools from .utils import * from .codes import * from .OBDResponse import Status, Test, Monitor, MonitorTest @@ -39,7 +40,6 @@ logger = logging.getLogger(__name__) - ''' All decoders take the form: @@ -50,6 +50,17 @@ def (): ''' + +def uas(id): + """ get the corresponding decoder for this UAS ID """ + return functools.partial(decode_uas, id=id) + +def decode_uas(messages, id): + d = messages[0].data + return UAS_IDS[id](d) + + + # drop all messages, return None def drop(messages): return None @@ -75,11 +86,6 @@ def raw_string(messages): Return Value object with value and units ''' -def count(messages): - d = messages[0].data - v = bytes_to_int(d) - return v * Unit.count - # 0 to 100 % def percent(messages): d = messages[0].data @@ -101,13 +107,6 @@ def temp(messages): v = v - 40 return Unit.Quantity(v, Unit.celsius) # non-multiplicative unit -# -40 to 6513.5 C -def catalyst_temp(messages): - d = messages[0].data - v = bytes_to_int(d) - v = (v / 10.0) - 40 - return Unit.Quantity(v, Unit.celsius) # non-multiplicative unit - # -128 to 128 mA def current_centered(messages): d = messages[0].data @@ -118,8 +117,7 @@ def current_centered(messages): # 0 to 1.275 volts def sensor_voltage(messages): d = messages[0].data - v = d[0] - v = v / 200.0 + v = d[0] / 200.0 return v * Unit.volt # 0 to 8 volts @@ -142,20 +140,6 @@ def pressure(messages): v = d[0] return v * Unit.kilopascal -# 0 to 5177 kPa -def fuel_pres_vac(messages): - d = messages[0].data - v = bytes_to_int(d) - v = v * 0.079 - return v * Unit.kilopascal - -# 0 to 655,350 kPa -def fuel_pres_direct(messages): - d = messages[0].data - v = bytes_to_int(d) - v = v * 10 - return v * Unit.kilopascal - # -8192 to 8192 Pa def evap_pressure(messages): # decode the twos complement @@ -179,18 +163,6 @@ def evap_pressure_alt(messages): v = v - 32767 return v * Unit.pascal -# 0 to 16,383.75 RPM -def rpm(messages): - d = messages[0].data - v = bytes_to_int(d) / 4.0 - return v * Unit.rpm - -# 0 to 255 KPH -def speed(messages): - d = messages[0].data - v = bytes_to_int(d) - return v * Unit.kph - # -64 to 63.5 degrees def timing_advance(messages): d = messages[0].data @@ -205,13 +177,6 @@ def inject_timing(messages): v = (v - 26880) / 128.0 return v * Unit.degree -# 0 to 655.35 grams/sec -def maf(messages): - d = messages[0].data - v = bytes_to_int(d) - v = v / 100.0 - return v * Unit.gps - # 0 to 2550 grams/sec def max_maf(messages): d = messages[0].data @@ -219,24 +184,6 @@ def max_maf(messages): v = v * 10 return v * Unit.gps -# 0 to 65535 seconds -def seconds(messages): - d = messages[0].data - v = bytes_to_int(d) - return v * Unit.second - -# 0 to 65535 minutes -def minutes(messages): - d = messages[0].data - v = bytes_to_int(d) - return v * Unit.minute - -# 0 to 65535 km -def distance(messages): - d = messages[0].data - v = bytes_to_int(d) - return v * Unit.kilometer - # 0 to 3212 Liters/hour def fuel_rate(messages): d = messages[0].data From dd28a3d075cebbaac885f71b3bff7f23ca0a2e7a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 14:15:38 -0400 Subject: [PATCH 393/569] removed test for removed decoders --- tests/test_decoders.py | 41 ----------------------------------------- 1 file changed, 41 deletions(-) diff --git a/tests/test_decoders.py b/tests/test_decoders.py index cd6bdb4a..66b7dcc2 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -45,11 +45,6 @@ def test_pid(): assert d.pid(m("F00AA00F")) == "11110000000010101010000000001111" assert d.pid(m("11")) == "00010001" -def test_count(): - assert d.count(m("00")) == 0 * Unit.count - assert d.count(m("0F")) == 15 * Unit.count - assert d.count(m("03E8")) == 1000 * Unit.count - def test_percent(): assert d.percent(m("00")) == 0.0 * Unit.percent assert d.percent(m("FF")) == 100.0 * Unit.percent @@ -64,10 +59,6 @@ def test_temp(): assert d.temp(m("FF")) == Unit.Quantity(215, Unit.celsius) assert d.temp(m("03E8")) == Unit.Quantity(960, Unit.celsius) -def test_catalyst_temp(): - assert d.catalyst_temp(m("0000")) == Unit.Quantity(-40.0, Unit.celsius) - assert d.catalyst_temp(m("FFFF")) == Unit.Quantity(6513.5, Unit.celsius) - def test_current_centered(): assert d.current_centered(m("00000000")) == -128.0 * Unit.milliampere assert d.current_centered(m("00008000")) == 0.0 * Unit.milliampere @@ -93,14 +84,6 @@ def test_pressure(): assert d.pressure(m("00")) == 0 * Unit.kilopascal assert d.pressure(m("00")) == 0 * Unit.kilopascal -def test_fuel_pres_vac(): - assert d.fuel_pres_vac(m("0000")) == 0.0 * Unit.kilopascal - assert d.fuel_pres_vac(m("FFFF")) == 5177.265 * Unit.kilopascal - -def test_fuel_pres_direct(): - assert d.fuel_pres_direct(m("0000")) == 0 * Unit.kilopascal - assert d.fuel_pres_direct(m("FFFF")) == 655350 * Unit.kilopascal - def test_evap_pressure(): pass # TODO #assert d.evap_pressure(m("0000")) == 0.0 * Unit.PA) @@ -114,14 +97,6 @@ def test_evap_pressure_alt(): assert d.evap_pressure_alt(m("7FFF")) == 0 * Unit.pascal assert d.evap_pressure_alt(m("FFFF")) == 32768 * Unit.pascal -def test_rpm(): - assert d.rpm(m("0000")) == 0.0 * Unit.rpm - assert d.rpm(m("FFFF")) == 16383.75 * Unit.rpm - -def test_speed(): - assert d.speed(m("00")) == 0 * Unit.kph - assert d.speed(m("FF")) == 255 * Unit.kph - def test_timing_advance(): assert d.timing_advance(m("00")) == -64.0 * Unit.degrees assert d.timing_advance(m("FF")) == 63.5 * Unit.degrees @@ -130,27 +105,11 @@ def test_inject_timing(): assert d.inject_timing(m("0000")) == -210 * Unit.degrees assert float_equals(d.inject_timing(m("FFFF")), 302 * Unit.degrees) -def test_maf(): - assert d.maf(m("0000")) == 0.0 * Unit.grams_per_second - assert d.maf(m("FFFF")) == 655.35 * Unit.grams_per_second - def test_max_maf(): assert d.max_maf(m("00000000")) == 0 * Unit.grams_per_second assert d.max_maf(m("FF000000")) == 2550 * Unit.grams_per_second assert d.max_maf(m("00ABCDEF")) == 0 * Unit.grams_per_second # last 3 bytes are unused (should be disregarded) -def test_seconds(): - assert d.seconds(m("0000")) == 0 * Unit.second - assert d.seconds(m("FFFF")) == 65535 * Unit.second - -def test_minutes(): - assert d.minutes(m("0000")) == 0 * Unit.minute - assert d.minutes(m("FFFF")) == 65535 * Unit.minute - -def test_distance(): - assert d.distance(m("0000")) == 0 * Unit.kilometer - assert d.distance(m("FFFF")) == 65535 * Unit.kilometer - def test_fuel_rate(): assert d.fuel_rate(m("0000")) == 0.0 * Unit.liters_per_hour assert d.fuel_rate(m("FFFF")) == 3276.75 * Unit.liters_per_hour From 9e3dc4b76fe16ebfcea40de06a4960b1da3b6c41 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 14:20:58 -0400 Subject: [PATCH 394/569] formatting and comments in decoders --- obd/decoders.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 55470b0c..74a1c0bf 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -51,16 +51,6 @@ def (): -def uas(id): - """ get the corresponding decoder for this UAS ID """ - return functools.partial(decode_uas, id=id) - -def decode_uas(messages, id): - d = messages[0].data - return UAS_IDS[id](d) - - - # drop all messages, return None def drop(messages): return None @@ -81,10 +71,26 @@ def pid(messages): def raw_string(messages): return "\n".join([m.raw() for m in messages]) -''' -Sensor decoders -Return Value object with value and units -''' + +""" +Some decoders are simple and are already implemented in the Units And Scaling +tables (used mainly for Mode 06). The uas() decoder is a wrapper for any +Unit/Scaling in that table, simply to avoid redundant code. +""" + +def uas(id): + """ get the corresponding decoder for this UAS ID """ + return functools.partial(decode_uas, id=id) + +def decode_uas(messages, id): + d = messages[0].data + return UAS_IDS[id](d) + + +""" +General sensor decoders +Return pint Quantities +""" # 0 to 100 % def percent(messages): From 09bfe428189777bd925bc308162757ac419909e9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 15:26:49 -0400 Subject: [PATCH 395/569] tweaked logging in elm327 --- obd/elm327.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/obd/elm327.py b/obd/elm327.py index bdad95d0..a6ebb4ae 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -330,6 +330,7 @@ def close(self): self.__protocol = None if self.__port is not None: + logger.info("closing port") self.__write(b"ATZ") self.__port.close() self.__port = None @@ -408,7 +409,7 @@ def __read(self): if not c: if attempts <= 0: - logger.info("Failed to read port, giving up") + logger.warning("Failed to read port, giving up") break logger.info("Failed to read port, trying again...") From b0f0403bf9257e6e1ad1ecce27ca7348116bdee7 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 15:52:52 -0400 Subject: [PATCH 396/569] forgot to bump up the version in __version__ --- obd/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/__version__.py b/obd/__version__.py index 700be4cb..1f199f19 100644 --- a/obd/__version__.py +++ b/obd/__version__.py @@ -1,2 +1,2 @@ -__version__ = '0.5.0' +__version__ = '0.6.0' From 56b642d9aea896e81a37394d4a5203b46fc8c5b3 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 16:03:47 -0400 Subject: [PATCH 397/569] read data in blocks, not single characters --- obd/elm327.py | 59 +++++++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index a6ebb4ae..20c224fb 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -54,6 +54,8 @@ class ELM327: ecus() """ + ELM_PROMPT = b'>' + _SUPPORTED_PROTOCOLS = { #"0" : None, # Automatic Mode. This isn't an actual protocol. If the # ELM reports this, then we don't have enough @@ -397,46 +399,47 @@ def __read(self): accumulates characters until the prompt character is seen returns a list of [/r/n] delimited strings """ + if not self.__port: + logger.info("cannot perform __read() when unconnected") + return "" attempts = 2 - buffer = b'' + buffer = bytearray() - if self.__port: - while True: - c = self.__port.read(1) + while True: + data = self.__port.read(self.__port.in_waiting or 1) - # if nothing was recieved - if not c: + # if nothing was recieved + if not data: - if attempts <= 0: - logger.warning("Failed to read port, giving up") - break + if attempts <= 0: + logger.warning("Failed to read port, giving up") + break - logger.info("Failed to read port, trying again...") - attempts -= 1 - continue + logger.info("Failed to read port, trying again...") + attempts -= 1 + continue - # end on chevron (ELM prompt character) - if c == b'>': - break + buffer.extend(data) - # skip null characters (ELM spec page 9) - if c == b'\x00': - continue + # end on chevron (ELM prompt character) + if self.ELM_PROMPT in buffer: + break - buffer += c # whatever is left must be part of the response - else: - logger.info("cannot perform __read() when unconnected") - return "" + # log, and remove the "bytearray( ... )" part + logger.debug("read: " + repr(buffer)[10:-1]) + + # clean out any null characters + buffer = re.sub(b"\x00", b"", buffer) - logger.debug("read: " + repr(buffer)) + # remove the prompt character + if buffer.endswith(self.ELM_PROMPT): + buffer = buffer[:-1] # convert bytes into a standard string - raw = buffer.decode() + string = buffer.decode() - # splits into lines - # removes empty lines - # removes trailing spaces - lines = [ s.strip() for s in re.split("[\r\n]", raw) if bool(s) ] + # splits into lines while removing empty lines and trailing spaces + lines = [ s.strip() for s in re.split("[\r\n]", string) if bool(s) ] return lines From 58d01d067562e6de93b942abf239870b3e596df1 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 17:04:24 -0400 Subject: [PATCH 398/569] decreased wait time in obdsim test --- tests/test_obdsim.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_obdsim.py b/tests/test_obdsim.py index abf4c429..3d856a95 100644 --- a/tests/test_obdsim.py +++ b/tests/test_obdsim.py @@ -3,7 +3,7 @@ import pytest from obd import commands, Unit -STANDARD_WAIT_TIME = 0.25 +STANDARD_WAIT_TIME = 0.1 @pytest.fixture(scope="module") From 303e6bebe4001d786c17a5238f8a295107489c97 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 17:12:34 -0400 Subject: [PATCH 399/569] skip obdsim tests if no port was specified --- tests/test_obdsim.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/test_obdsim.py b/tests/test_obdsim.py index 3d856a95..3dc46fb4 100644 --- a/tests/test_obdsim.py +++ b/tests/test_obdsim.py @@ -11,12 +11,6 @@ def obd(request): """provides an OBD connection object for obdsim""" import obd port = request.config.getoption("--port") - - # TODO: lookup how to fail inside of a fixture - if port is None: - print("Please run obdsim and use --port=") - exit(1) - return obd.OBD(port) @@ -25,23 +19,22 @@ def async(request): """provides an OBD *Async* connection object for obdsim""" import obd port = request.config.getoption("--port") - - # TODO: lookup how to fail inside of a fixture - if port is None: - print("Please run obdsim and use --port=") - exit(1) - return obd.Async(port) def good_rpm_response(r): return (r.value.u == Unit.rpm) and (r.value >= 0.0 * Unit.rpm) + +@pytest.mark.skipif(not pytest.config.getoption("--port"), + reason="needs --port= to run") def test_supports(obd): assert(len(obd.supported_commands) > 0) assert(obd.supports(commands.RPM)) +@pytest.mark.skipif(not pytest.config.getoption("--port"), + reason="needs --port= to run") def test_rpm(obd): r = obd.query(commands.RPM) assert(good_rpm_response(r)) @@ -49,6 +42,8 @@ def test_rpm(obd): # Async tests +@pytest.mark.skipif(not pytest.config.getoption("--port"), + reason="needs --port= to run") def test_async_query(async): rs = [] @@ -67,6 +62,8 @@ def test_async_query(async): assert(all([ good_rpm_response(r) for r in rs ])) +@pytest.mark.skipif(not pytest.config.getoption("--port"), + reason="needs --port= to run") def test_async_callback(async): rs = [] @@ -81,6 +78,8 @@ def test_async_callback(async): assert(all([ good_rpm_response(r) for r in rs ])) +@pytest.mark.skipif(not pytest.config.getoption("--port"), + reason="needs --port= to run") def test_async_paused(async): assert(not async.running) @@ -97,6 +96,8 @@ def test_async_paused(async): assert(not async.running) +@pytest.mark.skipif(not pytest.config.getoption("--port"), + reason="needs --port= to run") def test_async_unwatch(async): watched_rs = [] @@ -127,6 +128,8 @@ def test_async_unwatch(async): assert(all([ r.is_null() for r in unwatched_rs ])) +@pytest.mark.skipif(not pytest.config.getoption("--port"), + reason="needs --port= to run") def test_async_unwatch_callback(async): a_rs = [] From 449099b2793a8d8cb01389f84828f068c10bb11c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 17:21:02 -0400 Subject: [PATCH 400/569] removed unused test file --- tests/test_elm327.py | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 tests/test_elm327.py diff --git a/tests/test_elm327.py b/tests/test_elm327.py deleted file mode 100644 index 781befc6..00000000 --- a/tests/test_elm327.py +++ /dev/null @@ -1,4 +0,0 @@ - -from obd.protocols import ECU, SAE_J1850_PWM -from obd.elm327 import ELM327 - From a4752be86ddb37e0d4fef800ff362fc5772f15c8 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 17:31:16 -0400 Subject: [PATCH 401/569] updated docs on custom obd commands --- docs/Custom Commands.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/Custom Commands.md b/docs/Custom Commands.md index 150cb412..1b80fd74 100644 --- a/docs/Custom Commands.md +++ b/docs/Custom Commands.md @@ -5,8 +5,8 @@ If the command you need is not in python-OBDs tables, you can create a new `OBDC |----------------------|----------|----------------------------------------------------------------------------| | name | string | (human readability only) | | desc | string | (human readability only) | -| command | string | OBD command in hex (typically mode + PID | -| bytes | int | Number of bytes expected in response | +| command | bytes | OBD command in hex (typically mode + PID | +| bytes | int | Number of bytes expected in response (zero means unknown) | | decoder | callable | Function used for decoding messages from the OBD adapter | | ecu (optional) | ECU | ID of the ECU this command should listen to (`ECU.ALL` by default) | | fast (optional) | bool | Allows python-OBD to alter this command for efficieny (`False` by default) | @@ -21,13 +21,14 @@ from obd.protocols import ECU from obd.utils import bytes_to_int def rpm(messages): + """ decoder for RPM messages """ d = messages[0].data v = bytes_to_int(d) / 4.0 # helper function for converting byte arrays to ints - return (v, Unit.RPM) + return v * Unit.RPM # construct a Pint Quantity c = OBDCommand("RPM", \ # name "Engine RPM", \ # description - "010C", \ # command + b"010C", \ # command 2, \ # number of return bytes to expect rpm, \ # decoding function ECU.ENGINE, \ # (optional) ECU filter @@ -64,10 +65,10 @@ The `decoder` argument is a function of following form. ```python def (): ... - return (, ) + return ``` -Decoders are given a list of `Message` objects as an argument. If your decoder is called, this list is garaunteed to have at least one message object. Each `Message` object has a `data` property, which holds a parsed byte array, and is also garauteed to have the number of bytes specified by the command. +The return value of your decoder will be loaded into the `OBDResponse.value` field. Decoders are given a list of `Message` objects as an argument. If your decoder is called, this list is garaunteed to have at least one message object. Each `Message` object has a `data` property, which holds a parsed bytearray, and is also garauteed to have the number of bytes specified by the command. *NOTE: If you are transitioning from an older version of Python-OBD (where decoders were given raw hex strings as arguments), you can use the `Message.hex()` function as a patch.* @@ -75,7 +76,7 @@ Decoders are given a list of `Message` objects as an argument. If your decoder i def (messages): _hex = messages[0].hex() ... - return (, ) + return ``` *You can also access the original string sent by the adapter using the `Message.raw()` function.* From 10b660f0140c422f5eac82135d0f22705d490aea Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 17:33:34 -0400 Subject: [PATCH 402/569] condensed custom commands unsupported handling --- docs/Custom Commands.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/Custom Commands.md b/docs/Custom Commands.md index 1b80fd74..cef34172 100644 --- a/docs/Custom Commands.md +++ b/docs/Custom Commands.md @@ -38,16 +38,14 @@ c = OBDCommand("RPM", \ # name By default, custom commands will be treated as "unsupported by the vehicle". There are two ways to handle this: ```python -# use the `force` parameter when querying o = obd.OBD() + +# use the `force` parameter when querying o.query(c, force=True) -``` -or +# OR -```python # add your command to the set of supported commands -o = obd.OBD() o.supported_commands.add(c) o.query(c) ``` From 4a1e9ee0eec4129a88431280e244d065366f3fcc Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 17:36:13 -0400 Subject: [PATCH 403/569] simplified debug enable into a oneliner --- docs/Debug.md | 3 +-- docs/Troubleshooting.md | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/Debug.md b/docs/Debug.md index ea7fd9dc..0f929826 100644 --- a/docs/Debug.md +++ b/docs/Debug.md @@ -2,9 +2,8 @@ python-OBD uses python's builtin logging system. By default, it is setup to send ```python import obd -import logging -obd.logger.setLevel(logging.DEBUG) +obd.logger.setLevel(obd.logging.DEBUG) # enables all debug information ``` Or, to silence all logging output from python-OBD: diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index 317793ac..f2b9baca 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -4,8 +4,7 @@ If python-OBD is not working properly, the first thing you should do is enable debug output. Add the following line before your connection code to print all of the debug information to your console: ```python -import logging -obd.logger.setLevel(logging.DEBUG) +obd.logger.setLevel(obd.logging.DEBUG) ``` Here are some common logs from python-OBD, and their meanings: From dc157a39711e3df7d472ee053aa8d6b16a48202b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 18:25:06 -0400 Subject: [PATCH 404/569] tweaked serial code for modern pyserial API --- obd/elm327.py | 56 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 20c224fb..14579d00 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -114,11 +114,10 @@ def __init__(self, portname, baudrate, protocol): try: logger.info("Opening serial port '%s'" % portname) self.__port = serial.Serial(portname, \ - baudrate = baudrate, \ parity = serial.PARITY_NONE, \ stopbits = 1, \ - bytesize = 8, \ - timeout = 3) # seconds + bytesize = 8, + timeout = 3) # seconds logger.info("Serial port successfully opened on " + self.port_name()) except serial.SerialException as e: @@ -128,6 +127,10 @@ def __init__(self, portname, baudrate, protocol): self.__error(e) return + # ------------------------ find the ELM's baud ------------------------ + + if not self.set_baudrate(baudrate): + self.__error("Failed to set baudrate"); # ---------------------------- ATZ (reset) ---------------------------- try: @@ -159,14 +162,14 @@ def __init__(self, portname, baudrate, protocol): self.__status = OBDStatus.ELM_CONNECTED # try to communicate with the car, and load the correct protocol parser - if self.load_protocol(protocol): + if self.set_protocol(protocol): self.__status = OBDStatus.CAR_CONNECTED logger.info("Connection successful") else: logger.error("Connected to the adapter, but failed to connect to the vehicle") - def load_protocol(self, protocol): + def set_protocol(self, protocol): if protocol is not None: # an explicit protocol was specified if protocol not in self._SUPPORTED_PROTOCOLS: @@ -241,33 +244,44 @@ def auto_protocol(self): return False - def auto_baudrate(self): - """Detect, select, and return the baud rate at which a connected - ELM32x interface is operating. + def set_baudrate(self, baud): + if baud is None: + return self.auto_baudrate() + else: + self.__port.baudrate = baud + return True - Return None if the baud rate couldn't be determined. + + def auto_baudrate(self): + """ + Detect the baud rate at which a connected ELM32x interface is operating. + Returns boolean for success. """ + + # before we change the timout, save the "normal" value + timeout = self.__port.timeout + self.__port.timeout = 0.05 # we're only talking with the ELM, so things should go quickly + for baud in self._TRY_BAUDS: - self.port.setBaudrate(baud) - self.port.flushInput() - self.port.flushOutput() + self.__port.baudrate = baud + self.__port.flushInput() + self.__port.flushOutput() # Send a nonsense command to get a prompt back from the scanner # (an empty command runs the risk of repeating a dangerous command) # The first character might get eaten if the interface was busy, # so write a second one (again so that the lone CR doesn't repeat # the previous command) - port.write("\x7F\x7F\r") - port.set_timeout(timeout) - response = self.__read() + self.__port.write("\x7F\x7F\r\n") + response = self.port.read(1024) - if (response.endswith("\r\r>")): - #print "%d baud detected (%r)" % (baud, response) - break - else: - baud = None + # watch for the prompt character + if response.endswith(b">"): + return True + + self.__port.timeout = timeout - return baud + return False From 0b5ad60ae5557c3a2a8eb6ff57d757b45a881513 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 20:49:51 -0400 Subject: [PATCH 405/569] added debug statements, increased timeout --- obd/elm327.py | 13 ++++++++++--- obd/obd.py | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 14579d00..4a17eb63 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -241,6 +241,7 @@ def auto_protocol(self): return True # if we've come this far, then we have failed... + logger.error("Failed to determine protocol") return False @@ -258,9 +259,11 @@ def auto_baudrate(self): Returns boolean for success. """ + logger.debug("Choosing baudrate automatically") + # before we change the timout, save the "normal" value timeout = self.__port.timeout - self.__port.timeout = 0.05 # we're only talking with the ELM, so things should go quickly + self.__port.timeout = 0.3 # we're only talking with the ELM, so things should go quickly for baud in self._TRY_BAUDS: self.__port.baudrate = baud @@ -272,15 +275,19 @@ def auto_baudrate(self): # The first character might get eaten if the interface was busy, # so write a second one (again so that the lone CR doesn't repeat # the previous command) - self.__port.write("\x7F\x7F\r\n") - response = self.port.read(1024) + self.__port.write(b"\x7F\x7F\r\n") + self.__port.flush() + response = self.__port.read(1024) + logger.debug("Response from baud %d: %s" % (baud, repr(response))) # watch for the prompt character if response.endswith(b">"): + logger.debug("Choosing baud %d" % baud) return True self.__port.timeout = timeout + logger.debug("Failed to choose baud") return False diff --git a/obd/obd.py b/obd/obd.py index 8efbb52b..755de20a 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -47,7 +47,7 @@ class OBD(object): with it's assorted commands/sensors. """ - def __init__(self, portstr=None, baudrate=38400, protocol=None, fast=True): + def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True): self.port = None self.supported_commands = set(commands.base_commands()) self.fast = fast From 594f545be8d7db9ad55f7e9bb7b7779620497bad Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 21:24:29 -0400 Subject: [PATCH 406/569] don't bother with auto-baud when connecting to a pts --- obd/elm327.py | 11 ++++++++--- tests/test_obdsim.py | 6 ++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 4a17eb63..545c9939 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -247,7 +247,11 @@ def auto_protocol(self): def set_baudrate(self, baud): if baud is None: - return self.auto_baudrate() + # when connecting to pseudo terminal, don't bother with auto baud + if self.port_name().startswith("/dev/pts"): + return True + else: + return self.auto_baudrate() else: self.__port.baudrate = baud return True @@ -263,7 +267,7 @@ def auto_baudrate(self): # before we change the timout, save the "normal" value timeout = self.__port.timeout - self.__port.timeout = 0.3 # we're only talking with the ELM, so things should go quickly + self.__port.timeout = 0.1 # we're only talking with the ELM, so things should go quickly for baud in self._TRY_BAUDS: self.__port.baudrate = baud @@ -283,11 +287,12 @@ def auto_baudrate(self): # watch for the prompt character if response.endswith(b">"): logger.debug("Choosing baud %d" % baud) + self.__port.timeout = timeout # reinstate our original timeout return True - self.__port.timeout = timeout logger.debug("Failed to choose baud") + self.__port.timeout = timeout # reinstate our original timeout return False diff --git a/tests/test_obdsim.py b/tests/test_obdsim.py index 3dc46fb4..d94862dc 100644 --- a/tests/test_obdsim.py +++ b/tests/test_obdsim.py @@ -3,7 +3,7 @@ import pytest from obd import commands, Unit -STANDARD_WAIT_TIME = 0.1 +STANDARD_WAIT_TIME = 0.2 @pytest.fixture(scope="module") @@ -23,7 +23,9 @@ def async(request): def good_rpm_response(r): - return (r.value.u == Unit.rpm) and (r.value >= 0.0 * Unit.rpm) + return (not r.is_null()) and \ + (r.value.u == Unit.rpm) and \ + (r.value >= 0.0 * Unit.rpm) @pytest.mark.skipif(not pytest.config.getoption("--port"), From dcc99ec37e9ab16bfe53555214a67923b04626c7 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 21:30:58 -0400 Subject: [PATCH 407/569] bump to 0.5.1 --- obd/__version__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/__version__.py b/obd/__version__.py index 700be4cb..fb8cecd5 100644 --- a/obd/__version__.py +++ b/obd/__version__.py @@ -1,2 +1,2 @@ -__version__ = '0.5.0' +__version__ = '0.5.1' diff --git a/setup.py b/setup.py index 3b79abdc..84d7e893 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="obd", - version="0.5.0", + version="0.5.1", description=("Serial module for handling live sensor data from a vehicle's OBD-II port"), classifiers=[ "Operating System :: POSIX :: Linux", From b8535d47f357e7120eb80a7f9207950b6c70cc7e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 22:32:59 -0400 Subject: [PATCH 408/569] set async to auto-baud by default --- obd/async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/async.py b/obd/async.py index 9ea67b40..d64ec7c1 100644 --- a/obd/async.py +++ b/obd/async.py @@ -44,7 +44,7 @@ class Async(OBD): Specialized for asynchronous value reporting. """ - def __init__(self, portstr=None, baudrate=38400, protocol=None, fast=True): + def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True): super(Async, self).__init__(portstr, baudrate, protocol, fast) self.__commands = {} # key = OBDCommand, value = Response self.__callbacks = {} # key = OBDCommand, value = list of Functions From eec3bb5a7a8a210150144bdbe207a4cf4425442a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 3 Jul 2016 22:49:06 -0400 Subject: [PATCH 409/569] removed commands that were deprecated in v0.5.0 --- docs/Connections.md | 6 ------ obd/OBDCommand.py | 5 ----- obd/__init__.py | 2 +- obd/obd.py | 6 ------ obd/utils.py | 5 ----- 5 files changed, 1 insertion(+), 23 deletions(-) diff --git a/docs/Connections.md b/docs/Connections.md index 36965ab6..749e05ea 100644 --- a/docs/Connections.md +++ b/docs/Connections.md @@ -91,12 +91,6 @@ Returns the string name for the currently connected port (`"/dev/ttyUSB0"`). If --- -### get_port_name() - -**Deprecated:** use `port_name()` instead - ---- - ### supports(command) Returns a boolean for whether a command is supported by both the car and python-OBD diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 3b064856..2a3f2e09 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -78,11 +78,6 @@ def pid(self): else: return 0 - # TODO: remove later - @property - def supported(self): - logger.warning("OBDCommand.supported is deprecated. Use OBD.supports() instead") - return False def __call__(self, messages): diff --git a/obd/__init__.py b/obd/__init__.py index d95f5815..1a5c9d1b 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -43,7 +43,7 @@ from .OBDCommand import OBDCommand from .OBDResponse import OBDResponse from .protocols import ECU -from .utils import scan_serial, scanSerial, OBDStatus # TODO: scanSerial() deprecated +from .utils import scan_serial, OBDStatus from .UnitsAndScaling import Unit import logging diff --git a/obd/obd.py b/obd/obd.py index 755de20a..d3aad0b7 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -180,12 +180,6 @@ def protocol_id(self): return self.port.protocol_id() - def get_port_name(self): - # TODO: deprecated, remove later - logger.warning("OBD.get_port_name() is deprecated, use OBD.port_name() instead") - return self.port_name() - - def port_name(self): """ Returns the name of the currently connected port """ if self.port is not None: diff --git a/obd/utils.py b/obd/utils.py index 08ed6900..942c2fdc 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -151,8 +151,3 @@ def scan_serial(): available.append(port) return available - -# TODO: deprecated, remove later -def scanSerial(): - logger.warning("scanSerial() is deprecated, use scan_serial() instead") - return scan_serial() From 2f360d5c8cc62dc71bbe82d8bea65c8eb564e7ce Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 4 Jul 2016 14:31:25 -0400 Subject: [PATCH 410/569] added command validation function, check for mode 06 over non-CAN --- obd/async.py | 4 ++-- obd/obd.py | 23 ++++++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/obd/async.py b/obd/async.py index d64ec7c1..ecd43746 100644 --- a/obd/async.py +++ b/obd/async.py @@ -136,8 +136,8 @@ def watch(self, c, callback=None, force=False): logger.warning("Can't watch() while running, please use stop()") else: - if not force and not self.supports(c): - logger.warning("'%s' is not supported" % str(c)) + if not force and not self.test_cmd(c): + # self.test_cmd() will print warnings return # new command being watched, store the command diff --git a/obd/obd.py b/obd/obd.py index d3aad0b7..9d91a6bb 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -215,6 +215,24 @@ def supports(self, cmd): return cmd in self.supported_commands + def test_cmd(self, cmd): + """ + Returns a boolean for whether a command will + be sent without using force=True. + """ + # test if the command is supported + if not self.supports(cmd): + logger.warning("'%s' is not supported" % str(cmd)) + return False + + # mode 06 is only implemented for the CAN protocols + if cmd.mode == 6 and self.port.protocol_id() not in ["6", "7", "8", "9"]: + logger.warning("Mode 06 commands are only supported over CAN protocols") + return False + + return True + + def query(self, cmd, force=False): """ primary API function. Sends commands to the car, and @@ -225,11 +243,10 @@ def query(self, cmd, force=False): logger.warning("Query failed, no connection available") return OBDResponse() - if not force and not self.supports(cmd): - logger.warning("'%s' is not supported" % str(cmd)) + # if the user forces, skip all checks + if not force and not self.test_cmd(cmd): return OBDResponse() - # send command and retrieve message logger.info("Sending command: %s" % str(cmd)) cmd_string = self.__build_command_string(cmd) From 52e33a9e7bbbbb3bad1217b7f3484c83d37808cd Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 4 Jul 2016 14:34:11 -0400 Subject: [PATCH 411/569] fixed syntax error retrieving port status --- obd/obd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/obd.py b/obd/obd.py index 9d91a6bb..30436d2f 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -84,7 +84,7 @@ def __connect(self, portstr, baudrate, protocol): self.port = ELM327(portstr, baudrate, protocol) # if the connection failed, close it - if self.port.status == OBDStatus.NOT_CONNECTED: + if self.port.status() == OBDStatus.NOT_CONNECTED: # the ELM327 class will report its own errors self.close() @@ -138,7 +138,7 @@ def close(self): Closes the connection, and clears supported_commands """ - self.supported_commands = [] + self.supported_commands = set() if self.port is not None: logger.info("Closing connection") From 73b1d2f9488e80950dc0000cd16f04845a27a3d7 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 4 Jul 2016 14:37:34 -0400 Subject: [PATCH 412/569] return empty strings instead of sentences --- obd/elm327.py | 3 ++- obd/obd.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 545c9939..343420d3 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -329,7 +329,7 @@ def port_name(self): if self.__port is not None: return self.__port.portstr else: - return "No Port" + return "" def status(self): @@ -433,6 +433,7 @@ def __read(self): buffer = bytearray() while True: + # retrieve as much data as possible data = self.__port.read(self.__port.in_waiting or 1) # if nothing was recieved diff --git a/obd/obd.py b/obd/obd.py index 30436d2f..7d825e15 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -185,7 +185,7 @@ def port_name(self): if self.port is not None: return self.port.port_name() else: - return "Not connected to any port" + return "" def is_connected(self): From 46cf2a551a27d9c9765d8a34623dac7638cc7591 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 4 Jul 2016 14:43:35 -0400 Subject: [PATCH 413/569] renamed OBD.port --> OBD.interface --- obd/obd.py | 42 +++++++++++++++++++++--------------------- tests/test_OBD.py | 44 ++++++++++++++++++++++---------------------- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/obd/obd.py b/obd/obd.py index 7d825e15..97504ebc 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -48,7 +48,7 @@ class OBD(object): """ def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True): - self.port = None + self.interface = None self.supported_commands = set(commands.base_commands()) self.fast = fast self.__last_command = "" # used for running the previous command with a CR @@ -75,16 +75,16 @@ def __connect(self, portstr, baudrate, protocol): for port in portnames: logger.info("Attempting to use port: " + str(port)) - self.port = ELM327(port, baudrate, protocol) + self.interface = ELM327(port, baudrate, protocol) - if self.port.status() >= OBDStatus.ELM_CONNECTED: + if self.interface.status() >= OBDStatus.ELM_CONNECTED: break # success! stop searching for serial else: logger.info("Explicit port defined") - self.port = ELM327(portstr, baudrate, protocol) + self.interface = ELM327(portstr, baudrate, protocol) # if the connection failed, close it - if self.port.status() == OBDStatus.NOT_CONNECTED: + if self.interface.status() == OBDStatus.NOT_CONNECTED: # the ELM327 class will report its own errors self.close() @@ -140,50 +140,50 @@ def close(self): self.supported_commands = set() - if self.port is not None: + if self.interface is not None: logger.info("Closing connection") - self.port.close() - self.port = None + self.interface.close() + self.interface = None def status(self): """ returns the OBD connection status """ - if self.port is None: + if self.interface is None: return OBDStatus.NOT_CONNECTED else: - return self.port.status() + return self.interface.status() # not sure how useful this would be # def ecus(self): # """ returns a list of ECUs in the vehicle """ - # if self.port is None: + # if self.interface is None: # return [] # else: - # return self.port.ecus() + # return self.interface.ecus() def protocol_name(self): """ returns the name of the protocol being used by the ELM327 """ - if self.port is None: + if self.interface is None: return "" else: - return self.port.protocol_name() + return self.interface.protocol_name() def protocol_id(self): """ returns the ID of the protocol being used by the ELM327 """ - if self.port is None: + if self.interface is None: return "" else: - return self.port.protocol_id() + return self.interface.protocol_id() def port_name(self): """ Returns the name of the currently connected port """ - if self.port is not None: - return self.port.port_name() + if self.interface is not None: + return self.interface.port_name() else: return "" @@ -226,7 +226,7 @@ def test_cmd(self, cmd): return False # mode 06 is only implemented for the CAN protocols - if cmd.mode == 6 and self.port.protocol_id() not in ["6", "7", "8", "9"]: + if cmd.mode == 6 and self.interface.protocol_id() not in ["6", "7", "8", "9"]: logger.warning("Mode 06 commands are only supported over CAN protocols") return False @@ -250,7 +250,7 @@ def query(self, cmd, force=False): # send command and retrieve message logger.info("Sending command: %s" % str(cmd)) cmd_string = self.__build_command_string(cmd) - messages = self.port.send_and_parse(cmd_string) + messages = self.interface.send_and_parse(cmd_string) # if we're sending a new command, note it if cmd_string: @@ -269,7 +269,7 @@ def __build_command_string(self, cmd): # only wait for as many ECUs as we've seen if self.fast and cmd.fast: - cmd_string += str(len(self.port.ecus())).encode() + cmd_string += str(len(self.interface.ecus())).encode() # if we sent this last time, just send if self.fast and (cmd_string == self.__last_command): diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 35b22b81..81516f2f 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -76,7 +76,7 @@ def test_is_connected(): assert not o.is_connected() # our fake ELM class always returns success for connections - o.port = FakeELM("/dev/null") + o.interface = FakeELM("/dev/null") assert o.is_connected() @@ -88,17 +88,17 @@ def test_status(): o = obd.OBD("/dev/null") assert o.status() == OBDStatus.NOT_CONNECTED - o.port = None + o.interface = None assert o.status() == OBDStatus.NOT_CONNECTED # we can manually set our fake ELM class to test # the other values - o.port = FakeELM("/dev/null") + o.interface = FakeELM("/dev/null") - o.port._status = OBDStatus.ELM_CONNECTED + o.interface._status = OBDStatus.ELM_CONNECTED assert o.status() == OBDStatus.ELM_CONNECTED - o.port._status = OBDStatus.CAR_CONNECTED + o.interface._status = OBDStatus.CAR_CONNECTED assert o.status() == OBDStatus.CAR_CONNECTED @@ -121,31 +121,31 @@ def test_port_name(): same values as the underlying ELM327 class. """ o = obd.OBD("/dev/null") - o.port = FakeELM("/dev/null") - assert o.port_name() == o.port._portname + o.interface = FakeELM("/dev/null") + assert o.port_name() == o.interface._portname - o.port = FakeELM("A different port name") - assert o.port_name() == o.port._portname + o.interface = FakeELM("A different port name") + assert o.port_name() == o.interface._portname def test_protocol_name(): o = obd.OBD("/dev/null") - o.port = None + o.interface = None assert o.protocol_name() == "" - o.port = FakeELM("/dev/null") - assert o.protocol_name() == o.port.protocol_name() + o.interface = FakeELM("/dev/null") + assert o.protocol_name() == o.interface.protocol_name() def test_protocol_id(): o = obd.OBD("/dev/null") - o.port = None + o.interface = None assert o.protocol_id() == "" - o.port = FakeELM("/dev/null") - assert o.protocol_id() == o.port.protocol_id() + o.interface = FakeELM("/dev/null") + assert o.protocol_id() == o.interface.protocol_id() @@ -158,30 +158,30 @@ def test_protocol_id(): def test_force(): o = obd.OBD("/dev/null", fast=False) # disable the trailing response count byte - o.port = FakeELM("/dev/null") + o.interface = FakeELM("/dev/null") r = o.query(obd.commands.RPM) assert r.is_null() - assert o.port._test_last_command(None) + assert o.interface._test_last_command(None) r = o.query(obd.commands.RPM, force=True) assert not r.is_null() - assert o.port._test_last_command(obd.commands.RPM.command) + assert o.interface._test_last_command(obd.commands.RPM.command) # a command that isn't in python-OBD's tables r = o.query(command) assert r.is_null() - assert o.port._test_last_command(None) + assert o.interface._test_last_command(None) r = o.query(command, force=True) - assert o.port._test_last_command(command.command) + assert o.interface._test_last_command(command.command) def test_fast(): o = obd.OBD("/dev/null", fast=False) - o.port = FakeELM("/dev/null") + o.interface = FakeELM("/dev/null") assert command.fast o.query(command, force=True) # force since this command isn't in the tables - # assert o.port._test_last_command(command.command) + # assert o.interface._test_last_command(command.command) From 178e1a59318c9336377060728c0bc629ff0a2986 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 4 Jul 2016 19:03:12 -0400 Subject: [PATCH 414/569] added tests for unsigned units --- obd/UnitsAndScaling.py | 2 +- tests/test_uas.py | 375 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 tests/test_uas.py diff --git a/obd/UnitsAndScaling.py b/obd/UnitsAndScaling.py index f36fccb7..4fbe9f1e 100644 --- a/obd/UnitsAndScaling.py +++ b/obd/UnitsAndScaling.py @@ -133,7 +133,7 @@ def __call__(self, _bytes): 0x3E : UAS(False, 0.00006103516, Unit.millimeter ** 2), 0x3F : UAS(False, 0.01, Unit.liter), 0x40 : UAS(False, 1, Unit.ppm), - 0x41 : UAS(False, 0.1, Unit.microampere), + 0x41 : UAS(False, 0.01, Unit.microampere), # signed ----------------------------------------- 0x81 : UAS(True, 1, Unit.count), diff --git a/tests/test_uas.py b/tests/test_uas.py new file mode 100644 index 00000000..98fedbb3 --- /dev/null +++ b/tests/test_uas.py @@ -0,0 +1,375 @@ + +from binascii import unhexlify +from obd.UnitsAndScaling import Unit, UAS_IDS + + +# shim to convert human-readable hex into bytearray +def b(_hex): + return bytearray(unhexlify(_hex)) + +FLOAT_EQUALS_TOLERANCE = 0.025 + +# comparison for pint floating point values +def float_equals(va, vb): + units_match = (va.u == vb.u) + values_match = (abs(va.magnitude - vb.magnitude) < FLOAT_EQUALS_TOLERANCE) + return values_match and units_match + + +""" +Unsigned Units +""" + +def test_01(): + assert UAS_IDS[0x01](b("0000")) == 0 * Unit.count + assert UAS_IDS[0x01](b("0001")) == 1 * Unit.count + assert UAS_IDS[0x01](b("FFFF")) == 65535 * Unit.count + +def test_02(): + assert UAS_IDS[0x02](b("0000")) == 0 * Unit.count + assert UAS_IDS[0x02](b("0001")) == 0.1 * Unit.count + assert UAS_IDS[0x02](b("FFFF")) == 6553.5 * Unit.count + +def test_03(): + assert UAS_IDS[0x03](b("0000")) == 0 * Unit.count + assert UAS_IDS[0x03](b("0001")) == 0.01 * Unit.count + assert UAS_IDS[0x03](b("FFFF")) == 655.35 * Unit.count + +def test_04(): + assert UAS_IDS[0x04](b("0000")) == 0 * Unit.count + assert UAS_IDS[0x04](b("0001")) == 0.001 * Unit.count + assert UAS_IDS[0x04](b("FFFF")) == 65.535 * Unit.count + +def test_05(): + assert float_equals(UAS_IDS[0x05](b("0000")), 0 * Unit.count) + assert float_equals(UAS_IDS[0x05](b("0001")), 0.0000305 * Unit.count) + assert float_equals(UAS_IDS[0x05](b("FFFF")), 1.9999 * Unit.count) + +def test_06(): + assert float_equals(UAS_IDS[0x06](b("0000")), 0 * Unit.count) + assert float_equals(UAS_IDS[0x06](b("0001")), 0.000305 * Unit.count) + assert float_equals(UAS_IDS[0x06](b("FFFF")), 19.988 * Unit.count) + +def test_07(): + assert float_equals(UAS_IDS[0x07](b("0000")), 0 * Unit.rpm) + assert float_equals(UAS_IDS[0x07](b("0002")), 0.5 * Unit.rpm) + assert float_equals(UAS_IDS[0x07](b("FFFD")), 16383.25 * Unit.rpm) + assert float_equals(UAS_IDS[0x07](b("FFFF")), 16383.75 * Unit.rpm) + +def test_08(): + assert float_equals(UAS_IDS[0x08](b("0000")), 0 * Unit.kph) + assert float_equals(UAS_IDS[0x08](b("0064")), 1 * Unit.kph) + assert float_equals(UAS_IDS[0x08](b("03E7")), 9.99 * Unit.kph) + assert float_equals(UAS_IDS[0x08](b("FFFF")), 655.35 * Unit.kph) + +def test_09(): + assert float_equals(UAS_IDS[0x09](b("0000")), 0 * Unit.kph) + assert float_equals(UAS_IDS[0x09](b("0064")), 100 * Unit.kph) + assert float_equals(UAS_IDS[0x09](b("03E7")), 999 * Unit.kph) + assert float_equals(UAS_IDS[0x09](b("FFFF")), 65535 * Unit.kph) + +def test_0A(): + # the standard gives example values that don't line up perfectly + # with the scale. The last two tests here deviate from the standard + assert float_equals(UAS_IDS[0x0A](b("0000")), 0 * Unit.millivolt) + assert float_equals(UAS_IDS[0x0A](b("0001")), 0.122 * Unit.millivolt) + assert float_equals(UAS_IDS[0x0A](b("2004")), 999.912 * Unit.millivolt) # 1000.488 mV + assert float_equals(UAS_IDS[0x0A](b("FFFF")), 7995.27 * Unit.millivolt) # 7999 mV + +def test_0B(): + assert UAS_IDS[0x0B](b("0000")) == 0 * Unit.volt + assert UAS_IDS[0x0B](b("0001")) == 0.001 * Unit.volt + assert UAS_IDS[0x0B](b("FFFF")) == 65.535 * Unit.volt + +def test_0C(): + assert float_equals(UAS_IDS[0x0C](b("0000")), 0 * Unit.volt) + assert float_equals(UAS_IDS[0x0C](b("0001")), 0.01 * Unit.volt) + assert float_equals(UAS_IDS[0x0C](b("FFFF")), 655.350 * Unit.volt) + +def test_0D(): + assert float_equals(UAS_IDS[0x0D](b("0000")), 0 * Unit.milliampere) + assert float_equals(UAS_IDS[0x0D](b("0001")), 0.004 * Unit.milliampere) + assert float_equals(UAS_IDS[0x0D](b("8000")), 128 * Unit.milliampere) + assert float_equals(UAS_IDS[0x0D](b("FFFF")), 255.996 * Unit.milliampere) + +def test_0E(): + assert UAS_IDS[0x0E](b("0000")) == 0 * Unit.ampere + assert UAS_IDS[0x0E](b("8000")) == 32.768 * Unit.ampere + assert UAS_IDS[0x0E](b("FFFF")) == 65.535 * Unit.ampere + +def test_0F(): + assert UAS_IDS[0x0F](b("0000")) == 0 * Unit.ampere + assert UAS_IDS[0x0F](b("0001")) == 0.01 * Unit.ampere + assert UAS_IDS[0x0F](b("FFFF")) == 655.35 * Unit.ampere + +def test_10(): + assert UAS_IDS[0x10](b("0000")) == 0 * Unit.millisecond + assert UAS_IDS[0x10](b("8000")) == 32768 * Unit.millisecond + assert UAS_IDS[0x10](b("EA60")) == 60000 * Unit.millisecond + assert UAS_IDS[0x10](b("FFFF")) == 65535 * Unit.millisecond + +def test_11(): + assert UAS_IDS[0x11](b("0000")) == 0 * Unit.millisecond + assert UAS_IDS[0x11](b("8000")) == 3276800 * Unit.millisecond + assert UAS_IDS[0x11](b("EA60")) == 6000000 * Unit.millisecond + assert UAS_IDS[0x11](b("FFFF")) == 6553500 * Unit.millisecond + +def test_12(): + assert UAS_IDS[0x12](b("0000")) == 0 * Unit.second + assert UAS_IDS[0x12](b("003C")) == 60 * Unit.second + assert UAS_IDS[0x12](b("0E10")) == 3600 * Unit.second + assert UAS_IDS[0x12](b("FFFF")) == 65535 * Unit.second + +def test_13(): + assert UAS_IDS[0x13](b("0000")) == 0 * Unit.milliohm + assert UAS_IDS[0x13](b("0001")) == 1 * Unit.milliohm + assert UAS_IDS[0x13](b("8000")) == 32768 * Unit.milliohm + assert UAS_IDS[0x13](b("FFFF")) == 65535 * Unit.milliohm + +def test_14(): + assert UAS_IDS[0x14](b("0000")) == 0 * Unit.ohm + assert UAS_IDS[0x14](b("0001")) == 1 * Unit.ohm + assert UAS_IDS[0x14](b("8000")) == 32768 * Unit.ohm + assert UAS_IDS[0x14](b("FFFF")) == 65535 * Unit.ohm + +def test_15(): + assert UAS_IDS[0x15](b("0000")) == 0 * Unit.kiloohm + assert UAS_IDS[0x15](b("0001")) == 1 * Unit.kiloohm + assert UAS_IDS[0x15](b("8000")) == 32768 * Unit.kiloohm + assert UAS_IDS[0x15](b("FFFF")) == 65535 * Unit.kiloohm + +def test_16(): + assert UAS_IDS[0x16](b("0000")) == Unit.Quantity(-40, Unit.celsius) + assert UAS_IDS[0x16](b("0001")) == Unit.Quantity(-39.9, Unit.celsius) + assert UAS_IDS[0x16](b("00DC")) == Unit.Quantity(-18, Unit.celsius) + assert UAS_IDS[0x16](b("0190")) == Unit.Quantity(0, Unit.celsius) + assert UAS_IDS[0x16](b("FFFF")) == Unit.Quantity(6513.5, Unit.celsius) + +def test_17(): + assert UAS_IDS[0x17](b("0000")) == 0 * Unit.kilopascal + assert UAS_IDS[0x17](b("0001")) == 0.01 * Unit.kilopascal + assert UAS_IDS[0x17](b("FFFF")) == 655.35 * Unit.kilopascal + +def test_18(): + assert UAS_IDS[0x18](b("0000")) == 0 * Unit.kilopascal + assert UAS_IDS[0x18](b("0001")) == 0.0117 * Unit.kilopascal + assert UAS_IDS[0x18](b("FFFF")) == 766.7595 * Unit.kilopascal + +def test_19(): + assert UAS_IDS[0x19](b("0000")) == 0 * Unit.kilopascal + assert UAS_IDS[0x19](b("0001")) == 0.079 * Unit.kilopascal + assert UAS_IDS[0x19](b("FFFF")) == 5177.265 * Unit.kilopascal + +def test_1A(): + assert UAS_IDS[0x1A](b("0000")) == 0 * Unit.kilopascal + assert UAS_IDS[0x1A](b("0001")) == 1 * Unit.kilopascal + assert UAS_IDS[0x1A](b("FFFF")) == 65535 * Unit.kilopascal + +def test_1B(): + assert UAS_IDS[0x1B](b("0000")) == 0 * Unit.kilopascal + assert UAS_IDS[0x1B](b("0001")) == 10 * Unit.kilopascal + assert UAS_IDS[0x1B](b("FFFF")) == 655350 * Unit.kilopascal + +def test_1C(): + assert UAS_IDS[0x1C](b("0000")) == 0 * Unit.degree + assert UAS_IDS[0x1C](b("0001")) == 0.01 * Unit.degree + assert UAS_IDS[0x1C](b("8CA0")) == 360 * Unit.degree + assert UAS_IDS[0x1C](b("FFFF")) == 655.35 * Unit.degree + +def test_1D(): + assert UAS_IDS[0x1D](b("0000")) == 0 * Unit.degree + assert UAS_IDS[0x1D](b("0001")) == 0.5 * Unit.degree + assert UAS_IDS[0x1D](b("FFFF")) == 32767.5 * Unit.degree + +def test_1E(): + assert float_equals(UAS_IDS[0x1E](b("0000")), 0 * Unit.ratio) + assert float_equals(UAS_IDS[0x1E](b("8013")), 1 * Unit.ratio) + assert float_equals(UAS_IDS[0x1E](b("FFFF")), 1.999 * Unit.ratio) + +def test_1F(): + assert float_equals(UAS_IDS[0x1F](b("0000")), 0 * Unit.ratio) + assert float_equals(UAS_IDS[0x1F](b("0001")), 0.05 * Unit.ratio) + assert float_equals(UAS_IDS[0x1F](b("0014")), 1 * Unit.ratio) + assert float_equals(UAS_IDS[0x1F](b("0126")), 14.7 * Unit.ratio) + assert float_equals(UAS_IDS[0x1F](b("FFFF")), 3276.75 * Unit.ratio) + +def test_20(): + assert float_equals(UAS_IDS[0x20](b("0000")), 0 * Unit.ratio) + assert float_equals(UAS_IDS[0x20](b("0001")), 0.0039062 * Unit.ratio) + assert float_equals(UAS_IDS[0x20](b("FFFF")), 255.993 * Unit.ratio) + +def test_21(): + assert UAS_IDS[0x21](b("0000")) == 0 * Unit.millihertz + assert UAS_IDS[0x21](b("8000")) == 32768 * Unit.millihertz + assert UAS_IDS[0x21](b("FFFF")) == 65535 * Unit.millihertz + +def test_22(): + assert UAS_IDS[0x22](b("0000")) == 0 * Unit.hertz + assert UAS_IDS[0x22](b("8000")) == 32768 * Unit.hertz + assert UAS_IDS[0x22](b("FFFF")) == 65535 * Unit.hertz + +def test_23(): + assert UAS_IDS[0x23](b("0000")) == 0 * Unit.kilohertz + assert UAS_IDS[0x23](b("8000")) == 32768 * Unit.kilohertz + assert UAS_IDS[0x23](b("FFFF")) == 65535 * Unit.kilohertz + +def test_24(): + assert UAS_IDS[0x24](b("0000")) == 0 * Unit.count + assert UAS_IDS[0x24](b("0001")) == 1 * Unit.count + assert UAS_IDS[0x24](b("FFFF")) == 65535 * Unit.count + +def test_25(): + assert UAS_IDS[0x25](b("0000")) == 0 * Unit.kilometer + assert UAS_IDS[0x25](b("0001")) == 1 * Unit.kilometer + assert UAS_IDS[0x25](b("FFFF")) == 65535 * Unit.kilometer + +def test_26(): + assert UAS_IDS[0x26](b("0000")) == 0 * Unit.millivolt / Unit.millisecond + assert UAS_IDS[0x26](b("0001")) == 0.1 * Unit.millivolt / Unit.millisecond + assert UAS_IDS[0x26](b("FFFF")) == 6553.5 * Unit.millivolt / Unit.millisecond + +def test_27(): + assert UAS_IDS[0x27](b("0000")) == 0 * Unit.grams_per_second + assert UAS_IDS[0x27](b("0001")) == 0.01 * Unit.grams_per_second + assert UAS_IDS[0x27](b("FFFF")) == 655.35 * Unit.grams_per_second + +def test_28(): + assert UAS_IDS[0x28](b("0000")) == 0 * Unit.grams_per_second + assert UAS_IDS[0x28](b("0001")) == 1 * Unit.grams_per_second + assert UAS_IDS[0x28](b("FFFF")) == 65535 * Unit.grams_per_second + +def test_29(): + assert UAS_IDS[0x29](b("0000")) == 0 * Unit.pascal / Unit.second + assert UAS_IDS[0x29](b("0004")) == 1 * Unit.pascal / Unit.second + assert UAS_IDS[0x29](b("FFFF")) == 16383.75 * Unit.pascal / Unit.second # deviates from standard examples + +def test_2A(): + assert UAS_IDS[0x2A](b("0000")) == 0 * Unit.kilogram / Unit.hour + assert UAS_IDS[0x2A](b("0001")) == 0.001 * Unit.kilogram / Unit.hour + assert UAS_IDS[0x2A](b("FFFF")) == 65.535 * Unit.kilogram / Unit.hour + +def test_2B(): + assert UAS_IDS[0x2B](b("0000")) == 0 * Unit.count + assert UAS_IDS[0x2B](b("0001")) == 1 * Unit.count + assert UAS_IDS[0x2B](b("FFFF")) == 65535 * Unit.count + +def test_2C(): + assert UAS_IDS[0x2C](b("0000")) == 0 * Unit.gram + assert UAS_IDS[0x2C](b("0001")) == 0.01 * Unit.gram + assert UAS_IDS[0x2C](b("FFFF")) == 655.35 * Unit.gram + +def test_2D(): + assert UAS_IDS[0x2D](b("0000")) == 0 * Unit.milligram + assert UAS_IDS[0x2D](b("0001")) == 0.01 * Unit.milligram + assert UAS_IDS[0x2D](b("FFFF")) == 655.35 * Unit.milligram + +def test_2E(): + assert UAS_IDS[0x2E](b("0000")) == False + assert UAS_IDS[0x2E](b("0001")) == True + assert UAS_IDS[0x2E](b("FFFF")) == True + +def test_2F(): + assert UAS_IDS[0x2F](b("0000")) == 0 * Unit.percent + assert UAS_IDS[0x2F](b("0001")) == 0.01 * Unit.percent + assert UAS_IDS[0x2F](b("2710")) == 100 * Unit.percent + assert UAS_IDS[0x2F](b("FFFF")) == 655.35 * Unit.percent + +def test_30(): + assert float_equals(UAS_IDS[0x30](b("0000")), 0 * Unit.percent) + assert float_equals(UAS_IDS[0x30](b("0001")), 0.001526 * Unit.percent) + assert float_equals(UAS_IDS[0x30](b("FFFF")), 100.00641 * Unit.percent) + +def test_31(): + assert UAS_IDS[0x31](b("0000")) == 0 * Unit.liter + assert UAS_IDS[0x31](b("0001")) == 0.001 * Unit.liter + assert UAS_IDS[0x31](b("FFFF")) == 65.535 * Unit.liter + +def test_32(): + assert float_equals(UAS_IDS[0x32](b("0000")), 0 * Unit.inch) + assert float_equals(UAS_IDS[0x32](b("0010")), 0.0004883 * Unit.inch) + assert float_equals(UAS_IDS[0x32](b("0011")), 0.0005188 * Unit.inch) + assert float_equals(UAS_IDS[0x32](b("FFFF")), 1.9999695 * Unit.inch) + +def test_33(): + assert float_equals(UAS_IDS[0x33](b("0000")), 0 * Unit.ratio) + assert float_equals(UAS_IDS[0x33](b("0001")), 0.00024414 * Unit.ratio) + assert float_equals(UAS_IDS[0x33](b("1000")), 1.0 * Unit.ratio) + assert float_equals(UAS_IDS[0x33](b("E5BE")), 14.36 * Unit.ratio) + assert float_equals(UAS_IDS[0x33](b("FFFF")), 16.0 * Unit.ratio) + +def test_34(): + assert UAS_IDS[0x34](b("0000")) == 0 * Unit.minute + assert UAS_IDS[0x34](b("003C")) == 60 * Unit.minute + assert UAS_IDS[0x34](b("0E10")) == 3600 * Unit.minute + assert UAS_IDS[0x34](b("FFFF")) == 65535 * Unit.minute + +def test_35(): + assert UAS_IDS[0x35](b("0000")) == 0 * Unit.millisecond + assert UAS_IDS[0x35](b("8000")) == 327680 * Unit.millisecond + assert UAS_IDS[0x35](b("EA60")) == 600000 * Unit.millisecond + assert UAS_IDS[0x35](b("FFFF")) == 655350 * Unit.millisecond + +def test_36(): + assert UAS_IDS[0x36](b("0000")) == 0 * Unit.gram + assert UAS_IDS[0x36](b("0001")) == 0.01 * Unit.gram + assert UAS_IDS[0x36](b("FFFF")) == 655.35 * Unit.gram + +def test_37(): + assert UAS_IDS[0x37](b("0000")) == 0 * Unit.gram + assert UAS_IDS[0x37](b("0001")) == 0.1 * Unit.gram + assert UAS_IDS[0x37](b("FFFF")) == 6553.5 * Unit.gram + +def test_38(): + assert UAS_IDS[0x38](b("0000")) == 0 * Unit.gram + assert UAS_IDS[0x38](b("0001")) == 1 * Unit.gram + assert UAS_IDS[0x38](b("FFFF")) == 65535 * Unit.gram + +def test_39(): + assert float_equals(UAS_IDS[0x39](b("0000")), -327.68 * Unit.percent) + assert float_equals(UAS_IDS[0x39](b("58F0")), -100 * Unit.percent) + assert float_equals(UAS_IDS[0x39](b("7FFF")), -0.01 * Unit.percent) + assert float_equals(UAS_IDS[0x39](b("8000")), 0 * Unit.percent) + assert float_equals(UAS_IDS[0x39](b("8001")), 0.01 * Unit.percent) + assert float_equals(UAS_IDS[0x39](b("A710")), 100 * Unit.percent) + assert float_equals(UAS_IDS[0x39](b("FFFF")), 327.67 * Unit.percent) + +def test_3A(): + assert UAS_IDS[0x3A](b("0000")) == 0 * Unit.gram + assert UAS_IDS[0x3A](b("0001")) == 0.001 * Unit.gram + assert UAS_IDS[0x3A](b("FFFF")) == 65.535 * Unit.gram + +def test_3B(): + assert float_equals(UAS_IDS[0x3B](b("0000")), 0 * Unit.gram) + assert float_equals(UAS_IDS[0x3B](b("0001")), 0.0001 * Unit.gram) + assert float_equals(UAS_IDS[0x3B](b("FFFF")), 6.5535 * Unit.gram) + +def test_3C(): + assert UAS_IDS[0x3C](b("0000")) == 0 * Unit.microsecond + assert UAS_IDS[0x3C](b("8000")) == 3276.8 * Unit.microsecond + assert UAS_IDS[0x3C](b("EA60")) == 6000.0 * Unit.microsecond + assert UAS_IDS[0x3C](b("FFFF")) == 6553.5 * Unit.microsecond + +def test_3D(): + assert UAS_IDS[0x3D](b("0000")) == 0 * Unit.milliampere + assert UAS_IDS[0x3D](b("0001")) == 0.01 * Unit.milliampere + assert UAS_IDS[0x3D](b("FFFF")) == 655.35 * Unit.milliampere + +def test_3E(): + assert float_equals(UAS_IDS[0x3E](b("0000")), 0 * Unit.millimeter ** 2) + assert float_equals(UAS_IDS[0x3E](b("8000")), 1.9999 * Unit.millimeter ** 2) + assert float_equals(UAS_IDS[0x3E](b("FFFF")), 3.9999 * Unit.millimeter ** 2) + +def test_3F(): + assert UAS_IDS[0x3F](b("0000")) == 0 * Unit.liter + assert UAS_IDS[0x3F](b("0001")) == 0.01 * Unit.liter + assert UAS_IDS[0x3F](b("FFFF")) == 655.35 * Unit.liter + +def test_40(): + assert UAS_IDS[0x40](b("0000")) == 0 * Unit.ppm + assert UAS_IDS[0x40](b("0001")) == 1 * Unit.ppm + assert UAS_IDS[0x40](b("FFFF")) == 65535 * Unit.ppm + +def test_41(): + assert UAS_IDS[0x41](b("0000")) == 0 * Unit.microampere + assert UAS_IDS[0x41](b("0001")) == 0.01 * Unit.microampere + assert UAS_IDS[0x41](b("FFFF")) == 655.35 * Unit.microampere From 6f7894d9cd690a91b94b9375d37de5248d294232 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 4 Jul 2016 20:50:09 -0400 Subject: [PATCH 415/569] added tests for signed units --- tests/test_uas.py | 198 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) diff --git a/tests/test_uas.py b/tests/test_uas.py index 98fedbb3..142f9426 100644 --- a/tests/test_uas.py +++ b/tests/test_uas.py @@ -373,3 +373,201 @@ def test_41(): assert UAS_IDS[0x41](b("0000")) == 0 * Unit.microampere assert UAS_IDS[0x41](b("0001")) == 0.01 * Unit.microampere assert UAS_IDS[0x41](b("FFFF")) == 655.35 * Unit.microampere + + + + +""" +signed Units +""" + +def test_81(): + assert UAS_IDS[0x81](b("8000")) == -32768 * Unit.count + assert UAS_IDS[0x81](b("FFFF")) == -1 * Unit.count + assert UAS_IDS[0x81](b("0000")) == 0 * Unit.count + assert UAS_IDS[0x81](b("0001")) == 1 * Unit.count + assert UAS_IDS[0x81](b("7FFF")) == 32767 * Unit.count + +def test_82(): + assert UAS_IDS[0x82](b("8000")) == -3276.8 * Unit.count + assert UAS_IDS[0x82](b("FFFF")) == -0.1 * Unit.count + assert UAS_IDS[0x82](b("0000")) == 0 * Unit.count + assert UAS_IDS[0x82](b("0001")) == 0.1 * Unit.count + assert float_equals(UAS_IDS[0x82](b("7FFF")), 3276.7 * Unit.count) + +def test_83(): + assert UAS_IDS[0x83](b("8000")) == -327.68 * Unit.count + assert UAS_IDS[0x83](b("FFFF")) == -0.01 * Unit.count + assert UAS_IDS[0x83](b("0000")) == 0 * Unit.count + assert UAS_IDS[0x83](b("0001")) == 0.01 * Unit.count + assert float_equals(UAS_IDS[0x83](b("7FFF")), 327.67 * Unit.count) + +def test_84(): + assert UAS_IDS[0x84](b("8000")) == -32.768 * Unit.count + assert UAS_IDS[0x84](b("FFFF")) == -0.001 * Unit.count + assert UAS_IDS[0x84](b("0000")) == 0 * Unit.count + assert UAS_IDS[0x84](b("0001")) == 0.001 * Unit.count + assert float_equals(UAS_IDS[0x84](b("7FFF")), 32.767 * Unit.count) + +def test_85(): + assert float_equals(UAS_IDS[0x85](b("8000")), -0.9999995 * Unit.count) + assert float_equals(UAS_IDS[0x85](b("FFFF")), -0.0000305 * Unit.count) + assert float_equals(UAS_IDS[0x85](b("0000")), 0 * Unit.count) + assert float_equals(UAS_IDS[0x85](b("0001")), 0.0000305 * Unit.count) + assert float_equals(UAS_IDS[0x85](b("7FFF")), 0.9999995 * Unit.count) + +def test_86(): + assert float_equals(UAS_IDS[0x86](b("8000")), -9.999995 * Unit.count) + assert float_equals(UAS_IDS[0x86](b("FFFF")), -0.000305 * Unit.count) + assert float_equals(UAS_IDS[0x86](b("0000")), 0 * Unit.count) + assert float_equals(UAS_IDS[0x86](b("0001")), 0.000305 * Unit.count) + assert float_equals(UAS_IDS[0x86](b("7FFF")), 9.999995 * Unit.count) + +def test_87(): + assert UAS_IDS[0x87](b("8000")) == -32768 * Unit.ppm + assert UAS_IDS[0x87](b("FFFF")) == -1 * Unit.ppm + assert UAS_IDS[0x87](b("0000")) == 0 * Unit.ppm + assert UAS_IDS[0x87](b("0001")) == 1 * Unit.ppm + assert UAS_IDS[0x87](b("7FFF")) == 32767 * Unit.ppm + +def test_8A(): + # the standard gives example values that don't line up perfectly + # with the scale. The last two tests here deviate from the standard + assert float_equals(UAS_IDS[0x8A](b("8000")), -3997.696 * Unit.millivolt) # -3999.998 mV + assert float_equals(UAS_IDS[0x8A](b("FFFF")), -0.122 * Unit.millivolt) + assert float_equals(UAS_IDS[0x8A](b("0000")), 0 * Unit.millivolt) + assert float_equals(UAS_IDS[0x8A](b("0001")), 0.122 * Unit.millivolt) + assert float_equals(UAS_IDS[0x8A](b("7FFF")), 3997.574 * Unit.millivolt) # 3999.876 mV + +def test_8B(): + assert UAS_IDS[0x8B](b("8000")) == -32.768 * Unit.volt + assert UAS_IDS[0x8B](b("FFFF")) == -0.001 * Unit.volt + assert UAS_IDS[0x8B](b("0000")) == 0 * Unit.volt + assert UAS_IDS[0x8B](b("0001")) == 0.001 * Unit.volt + assert UAS_IDS[0x8B](b("7FFF")) == 32.767 * Unit.volt + +def test_8C(): + assert UAS_IDS[0x8C](b("8000")) == -327.68 * Unit.volt + assert UAS_IDS[0x8C](b("FFFF")) == -0.01 * Unit.volt + assert UAS_IDS[0x8C](b("0000")) == 0 * Unit.volt + assert UAS_IDS[0x8C](b("0001")) == 0.01 * Unit.volt + assert UAS_IDS[0x8C](b("7FFF")) == 327.67 * Unit.volt + +def test_8D(): + assert float_equals(UAS_IDS[0x8D](b("8000")), -128 * Unit.milliampere) + assert float_equals(UAS_IDS[0x8D](b("FFFF")), -0.00390625 * Unit.milliampere) + assert float_equals(UAS_IDS[0x8D](b("0000")), 0 * Unit.milliampere) + assert float_equals(UAS_IDS[0x8D](b("0001")), 0.00390625 * Unit.milliampere) + assert float_equals(UAS_IDS[0x8D](b("7FFF")), 127.996 * Unit.milliampere) + +def test_8E(): + assert UAS_IDS[0x8E](b("8000")) == -32.768 * Unit.ampere + assert UAS_IDS[0x8E](b("FFFF")) == -0.001 * Unit.ampere + assert UAS_IDS[0x8E](b("0000")) == 0 * Unit.ampere + assert UAS_IDS[0x8E](b("0001")) == 0.001 * Unit.ampere + assert UAS_IDS[0x8E](b("7FFF")) == 32.767 * Unit.ampere + +def test_90(): + assert UAS_IDS[0x90](b("8000")) == -32768 * Unit.millisecond + assert UAS_IDS[0x90](b("FFFF")) == -1 * Unit.millisecond + assert UAS_IDS[0x90](b("0000")) == 0 * Unit.millisecond + assert UAS_IDS[0x90](b("0001")) == 1 * Unit.millisecond + assert UAS_IDS[0x90](b("7FFF")) == 32767 * Unit.millisecond + +def test_96(): + assert float_equals(UAS_IDS[0x96](b("8000")), Unit.Quantity(-3276.8, Unit.celsius)) + assert float_equals(UAS_IDS[0x96](b("FFFF")), Unit.Quantity(-0.1, Unit.celsius)) + assert float_equals(UAS_IDS[0x96](b("0000")), Unit.Quantity(0, Unit.celsius)) + assert float_equals(UAS_IDS[0x96](b("0001")), Unit.Quantity(0.1, Unit.celsius)) + assert float_equals(UAS_IDS[0x96](b("7FFF")), Unit.Quantity(3276.7, Unit.celsius)) + +def test_99(): + assert float_equals(UAS_IDS[0x99](b("8000")), -3276.8 * Unit.kilopascal) + assert float_equals(UAS_IDS[0x99](b("FFFF")), -0.1 * Unit.kilopascal) + assert float_equals(UAS_IDS[0x99](b("0000")), 0 * Unit.kilopascal) + assert float_equals(UAS_IDS[0x99](b("0001")), 0.1 * Unit.kilopascal) + assert float_equals(UAS_IDS[0x99](b("7FFF")), 3276.7 * Unit.kilopascal) + +def test_9C(): + assert UAS_IDS[0x9C](b("8000")) == -327.68 * Unit.degree + assert UAS_IDS[0x9C](b("FFFF")) == -0.01 * Unit.degree + assert UAS_IDS[0x9C](b("0000")) == 0 * Unit.degree + assert UAS_IDS[0x9C](b("0001")) == 0.01 * Unit.degree + assert UAS_IDS[0x9C](b("7FFF")) == 327.67 * Unit.degree + +def test_9D(): + assert UAS_IDS[0x9D](b("8000")) == -16384 * Unit.degree + assert UAS_IDS[0x9D](b("FFFF")) == -0.5 * Unit.degree + assert UAS_IDS[0x9D](b("0000")) == 0 * Unit.degree + assert UAS_IDS[0x9D](b("0001")) == 0.5 * Unit.degree + assert UAS_IDS[0x9D](b("7FFF")) == 16383.5 * Unit.degree + +def test_A8(): + assert UAS_IDS[0xA8](b("8000")) == -32768 * Unit.grams_per_second + assert UAS_IDS[0xA8](b("FFFF")) == -1 * Unit.grams_per_second + assert UAS_IDS[0xA8](b("0000")) == 0 * Unit.grams_per_second + assert UAS_IDS[0xA8](b("0001")) == 1 * Unit.grams_per_second + assert UAS_IDS[0xA8](b("7FFF")) == 32767 * Unit.grams_per_second + +def test_A9(): + assert UAS_IDS[0xA9](b("8000")) == -8192 * Unit.pascal / Unit.second + assert UAS_IDS[0xA9](b("FFFC")) == -1 * Unit.pascal / Unit.second + assert UAS_IDS[0xA9](b("0000")) == 0 * Unit.pascal / Unit.second + assert UAS_IDS[0xA9](b("0004")) == 1 * Unit.pascal / Unit.second + assert UAS_IDS[0xA9](b("7FFF")) == 8191.75 * Unit.pascal / Unit.second + +def test_AD(): + assert UAS_IDS[0xAD](b("8000")) == -327.68 * Unit.milligram + assert UAS_IDS[0xAD](b("FFFF")) == -0.01 * Unit.milligram + assert UAS_IDS[0xAD](b("0000")) == 0 * Unit.milligram + assert UAS_IDS[0xAD](b("0001")) == 0.01 * Unit.milligram + assert UAS_IDS[0xAD](b("7FFF")) == 327.67 * Unit.milligram + +def test_AE(): + assert UAS_IDS[0xAE](b("8000")) == -3276.8 * Unit.milligram + assert UAS_IDS[0xAE](b("FFFF")) == -0.1 * Unit.milligram + assert UAS_IDS[0xAE](b("0000")) == 0 * Unit.milligram + assert UAS_IDS[0xAE](b("0001")) == 0.1 * Unit.milligram + assert float_equals(UAS_IDS[0xAE](b("7FFF")), 3276.7 * Unit.milligram) + +def test_AF(): + assert UAS_IDS[0xAF](b("8000")) == -327.68 * Unit.percent + assert UAS_IDS[0xAF](b("FFFF")) == -0.01 * Unit.percent + assert UAS_IDS[0xAF](b("0000")) == 0 * Unit.percent + assert UAS_IDS[0xAF](b("0001")) == 0.01 * Unit.percent + assert UAS_IDS[0xAF](b("7FFF")) == 327.67 * Unit.percent + +def test_B0(): + assert UAS_IDS[0xB0](b("8000")) == -100.007936 * Unit.percent + assert UAS_IDS[0xB0](b("FFFF")) == -0.003052 * Unit.percent + assert UAS_IDS[0xB0](b("0000")) == 0 * Unit.percent + assert UAS_IDS[0xB0](b("0001")) == 0.003052 * Unit.percent + assert UAS_IDS[0xB0](b("7FFF")) == 100.004884 * Unit.percent + +def test_B1(): + assert UAS_IDS[0xB1](b("8000")) == -65536 * Unit.millivolt / Unit.second + assert UAS_IDS[0xB1](b("FFFF")) == -2 * Unit.millivolt / Unit.second + assert UAS_IDS[0xB1](b("0000")) == 0 * Unit.millivolt / Unit.second + assert UAS_IDS[0xB1](b("0001")) == 2 * Unit.millivolt / Unit.second + assert UAS_IDS[0xB1](b("7FFF")) == 65534 * Unit.millivolt / Unit.second + +def test_FC(): + assert UAS_IDS[0xFC](b("8000")) == -327.68 * Unit.kilopascal + assert UAS_IDS[0xFC](b("FFFF")) == -0.01 * Unit.kilopascal + assert UAS_IDS[0xFC](b("0000")) == 0 * Unit.kilopascal + assert UAS_IDS[0xFC](b("0001")) == 0.01 * Unit.kilopascal + assert UAS_IDS[0xFC](b("7FFF")) == 327.67 * Unit.kilopascal + +def test_FD(): + assert UAS_IDS[0xFD](b("8000")) == -32.768 * Unit.kilopascal + assert UAS_IDS[0xFD](b("FFFF")) == -0.001 * Unit.kilopascal + assert UAS_IDS[0xFD](b("0000")) == 0 * Unit.kilopascal + assert UAS_IDS[0xFD](b("0001")) == 0.001 * Unit.kilopascal + assert UAS_IDS[0xFD](b("7FFF")) == 32.767 * Unit.kilopascal + +def test_FE(): + assert UAS_IDS[0xFE](b("8000")) == -8192 * Unit.pascal + assert UAS_IDS[0xFE](b("FFFC")) == -1 * Unit.pascal + assert UAS_IDS[0xFE](b("0000")) == 0 * Unit.pascal + assert UAS_IDS[0xFE](b("0004")) == 1 * Unit.pascal + assert UAS_IDS[0xFE](b("7FFF")) == 8191.75 * Unit.pascal From 8ad96eeef94c1531e6c62802e6f4f00ec553a6bb Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 4 Jul 2016 22:22:20 -0400 Subject: [PATCH 416/569] skip mode 02 PID getters, added MID getters --- obd/commands.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/obd/commands.py b/obd/commands.py index d23bab5c..cb0259eb 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -160,6 +160,8 @@ c.command = b"02" + c.command[2:] # change the mode: 0100 ---> 0200 c.name = "DTC_" + c.name c.desc = "DTC " + c.desc + if c.decode == pid: + c.decode = drop # Never send mode 02 pid requests (use mode 01 instead) __mode2__.append(c) @@ -365,6 +367,7 @@ def base_commands(self): """ return [ self.PIDS_A, + self.MIDS_A, self.GET_DTC, self.CLEAR_DTC, self.GET_FREEZE_DTC, From 2f6ecca1ed4772edabcd16a3cfeb416567de8b8a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 4 Jul 2016 22:30:31 -0400 Subject: [PATCH 417/569] simplified PID getter enumeration --- obd/commands.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index cb0259eb..079cecab 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -380,14 +380,7 @@ def pid_getters(self): """ returns a list of PID GET commands """ getters = [] for mode in self.modes: - for cmd in mode: - - if cmd is None: - continue # this command is reserved - - if cmd.decode == pid: # GET commands have a special decoder - getters.append(cmd) - + getters += [ cmd for cmd in mode if (cmd and cmd.decode == pid) ] return getters From b11cc23df68bbd849d5e4ac8e7b7e26f9a7f9353 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 4 Jul 2016 22:44:48 -0400 Subject: [PATCH 418/569] don't bother type checking everything --- obd/commands.py | 45 ++++++++++++++---------------------------- tests/test_commands.py | 3 +-- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 079cecab..574f5e10 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -349,15 +349,12 @@ def __getitem__(self, key): def __len__(self): """ returns the number of commands supported by python-OBD """ - l = 0 - for m in self.modes: - l += len(m) - return l + return sum([len(mode) for mode in self.modes]) - def __contains__(self, s): + def __contains__(self, name): """ calls has_name(s) """ - return self.has_name(s) + return self.has_name(name) def base_commands(self): @@ -395,38 +392,26 @@ def set_supported(self, mode, pid, v): def has_command(self, c): """ checks for existance of a command by OBDCommand object """ - if isinstance(c, OBDCommand): - return c in self.__dict__.values() - else: - logger.warning("has_command() only accepts OBDCommand objects") - return False + return c in self.__dict__.values() - def has_name(self, s): + def has_name(self, name): """ checks for existance of a command by name """ - if isinstance(s, str) or isinstance(s, unicode): - return s.isupper() and (s in self.__dict__.keys()) - else: - logger.warning("has_name() only accepts string names for commands") - return False + # isupper() rejects all the normal properties + return name.isupper() and name in self.__dict__ def has_pid(self, mode, pid): """ checks for existance of a command by int mode and int pid """ - if isinstance(mode, int) and isinstance(pid, int): - if (mode < 0) or (pid < 0): - return False - if mode >= len(self.modes): - return False - if pid >= len(self.modes[mode]): - return False - - # make sure that the command isn't reserved - return (self.modes[mode][pid] is not None) - - else: - logger.warning("has_pid() only accepts integer values for mode and PID") + if (mode < 0) or (pid < 0): + return False + if mode >= len(self.modes): return False + if pid >= len(self.modes[mode]): + return False + + # make sure that the command isn't reserved + return (self.modes[mode][pid] is not None) # export this object diff --git a/tests/test_commands.py b/tests/test_commands.py index dac06ade..1b257d55 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -80,11 +80,10 @@ def test_contains(): # by `in` assert cmd.name in obd.commands - # test things NOT in the tables, or invalid parameters + # test things NOT in the tables assert 'modes' not in obd.commands assert not obd.commands.has_pid(-1, 0) assert not obd.commands.has_pid(1, -1) - assert not obd.commands.has_command("I'm a string, not an OBDCommand") def test_pid_getters(): From 70deb94e22d76c03d5bc8ce756d1fb0f4142f6ff Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 5 Jul 2016 14:02:19 -0400 Subject: [PATCH 419/569] corrected name/description for mode 07 --- obd/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 574f5e10..a416d3ca 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -280,8 +280,8 @@ ] __mode7__ = [ - # name description cmd bytes decoder ECU fast - OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , b"07", 0, dtc, ECU.ALL, False), + # name description cmd bytes decoder ECU fast + OBDCommand("GET_CURRENT_DTC" , "Get DTCs from the current/last driving cycle" , b"07", 0, dtc, ECU.ALL, False), ] __mode9__ = [ From bbd14e4f6ca5cc1d0af637109e0b442fa8e08050 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 5 Jul 2016 14:13:43 -0400 Subject: [PATCH 420/569] updated command docs --- docs/Commands.md | 113 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 3 deletions(-) diff --git a/docs/Commands.md b/docs/Commands.md index 8d422bf3..232b99b2 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -222,12 +222,119 @@ example output:
+# Mode 06 + +Mode 06 commands are used to monitor various test results from the vehicle. Currently, Mode 06 commands are only implemented for CAN protocols (ISO 15765-4). + +|PID | Name | Description | +|-------|-----------------------------|--------------------------------------------| +| 00 | MIDS_A | Supported MIDs [01-20] | +| 01 | MONITOR_O2_B1S1 | O2 Sensor Monitor Bank 1 - Sensor 1 | +| 02 | MONITOR_O2_B1S2 | O2 Sensor Monitor Bank 1 - Sensor 2 | +| 03 | MONITOR_O2_B1S3 | O2 Sensor Monitor Bank 1 - Sensor 3 | +| 04 | MONITOR_O2_B1S4 | O2 Sensor Monitor Bank 1 - Sensor 4 | +| 05 | MONITOR_O2_B2S1 | O2 Sensor Monitor Bank 2 - Sensor 1 | +| 06 | MONITOR_O2_B2S2 | O2 Sensor Monitor Bank 2 - Sensor 2 | +| 07 | MONITOR_O2_B2S3 | O2 Sensor Monitor Bank 2 - Sensor 3 | +| 08 | MONITOR_O2_B2S4 | O2 Sensor Monitor Bank 2 - Sensor 4 | +| 09 | MONITOR_O2_B3S1 | O2 Sensor Monitor Bank 3 - Sensor 1 | +| 0A | MONITOR_O2_B3S2 | O2 Sensor Monitor Bank 3 - Sensor 2 | +| 0B | MONITOR_O2_B3S3 | O2 Sensor Monitor Bank 3 - Sensor 3 | +| 0C | MONITOR_O2_B3S4 | O2 Sensor Monitor Bank 3 - Sensor 4 | +| 0D | MONITOR_O2_B4S1 | O2 Sensor Monitor Bank 4 - Sensor 1 | +| 0E | MONITOR_O2_B4S2 | O2 Sensor Monitor Bank 4 - Sensor 2 | +| 0F | MONITOR_O2_B4S3 | O2 Sensor Monitor Bank 4 - Sensor 3 | +| 10 | MONITOR_O2_B4S4 | O2 Sensor Monitor Bank 4 - Sensor 4 | +| *gap* | | | +| 20 | MIDS_B | Supported MIDs [21-40] | +| 21 | MONITOR_CATALYST_B1 | Catalyst Monitor Bank 1 | +| 22 | MONITOR_CATALYST_B2 | Catalyst Monitor Bank 2 | +| 23 | MONITOR_CATALYST_B3 | Catalyst Monitor Bank 3 | +| 24 | MONITOR_CATALYST_B4 | Catalyst Monitor Bank 4 | +| *gap* | | | +| 31 | MONITOR_EGR_B1 | EGR Monitor Bank 1 | +| 32 | MONITOR_EGR_B2 | EGR Monitor Bank 2 | +| 33 | MONITOR_EGR_B3 | EGR Monitor Bank 3 | +| 34 | MONITOR_EGR_B4 | EGR Monitor Bank 4 | +| 35 | MONITOR_VVT_B1 | VVT Monitor Bank 1 | +| 36 | MONITOR_VVT_B2 | VVT Monitor Bank 2 | +| 37 | MONITOR_VVT_B3 | VVT Monitor Bank 3 | +| 38 | MONITOR_VVT_B4 | VVT Monitor Bank 4 | +| 39 | MONITOR_EVAP_150 | EVAP Monitor (Cap Off / 0.150\") | +| 3A | MONITOR_EVAP_090 | EVAP Monitor (0.090\") | +| 3B | MONITOR_EVAP_040 | EVAP Monitor (0.040\") | +| 3C | MONITOR_EVAP_020 | EVAP Monitor (0.020\") | +| 3D | MONITOR_PURGE_FLOW | Purge Flow Monitor | +| *gap* | | | +| 40 | MIDS_C | Supported MIDs [41-60] | +| 41 | MONITOR_O2_HEATER_B1S1 | O2 Sensor Heater Monitor Bank 1 - Sensor 1 | +| 42 | MONITOR_O2_HEATER_B1S2 | O2 Sensor Heater Monitor Bank 1 - Sensor 2 | +| 43 | MONITOR_O2_HEATER_B1S3 | O2 Sensor Heater Monitor Bank 1 - Sensor 3 | +| 44 | MONITOR_O2_HEATER_B1S4 | O2 Sensor Heater Monitor Bank 1 - Sensor 4 | +| 45 | MONITOR_O2_HEATER_B2S1 | O2 Sensor Heater Monitor Bank 2 - Sensor 1 | +| 46 | MONITOR_O2_HEATER_B2S2 | O2 Sensor Heater Monitor Bank 2 - Sensor 2 | +| 47 | MONITOR_O2_HEATER_B2S3 | O2 Sensor Heater Monitor Bank 2 - Sensor 3 | +| 48 | MONITOR_O2_HEATER_B2S4 | O2 Sensor Heater Monitor Bank 2 - Sensor 4 | +| 49 | MONITOR_O2_HEATER_B3S1 | O2 Sensor Heater Monitor Bank 3 - Sensor 1 | +| 4A | MONITOR_O2_HEATER_B3S2 | O2 Sensor Heater Monitor Bank 3 - Sensor 2 | +| 4B | MONITOR_O2_HEATER_B3S3 | O2 Sensor Heater Monitor Bank 3 - Sensor 3 | +| 4C | MONITOR_O2_HEATER_B3S4 | O2 Sensor Heater Monitor Bank 3 - Sensor 4 | +| 4D | MONITOR_O2_HEATER_B4S1 | O2 Sensor Heater Monitor Bank 4 - Sensor 1 | +| 4E | MONITOR_O2_HEATER_B4S2 | O2 Sensor Heater Monitor Bank 4 - Sensor 2 | +| 4F | MONITOR_O2_HEATER_B4S3 | O2 Sensor Heater Monitor Bank 4 - Sensor 3 | +| 50 | MONITOR_O2_HEATER_B4S4 | O2 Sensor Heater Monitor Bank 4 - Sensor 4 | +| *gap* | | | +| 60 | MIDS_D | Supported MIDs [61-80] | +| 61 | MONITOR_HEATED_CATALYST_B1 | Heated Catalyst Monitor Bank 1 | +| 62 | MONITOR_HEATED_CATALYST_B2 | Heated Catalyst Monitor Bank 2 | +| 63 | MONITOR_HEATED_CATALYST_B3 | Heated Catalyst Monitor Bank 3 | +| 64 | MONITOR_HEATED_CATALYST_B4 | Heated Catalyst Monitor Bank 4 | +| *gap* | | | +| 71 | MONITOR_SECONDARY_AIR_1 | Secondary Air Monitor 1 | +| 72 | MONITOR_SECONDARY_AIR_2 | Secondary Air Monitor 2 | +| 73 | MONITOR_SECONDARY_AIR_3 | Secondary Air Monitor 3 | +| 74 | MONITOR_SECONDARY_AIR_4 | Secondary Air Monitor 4 | +| *gap* | | | +| 80 | MIDS_E | Supported MIDs [81-A0] | +| 81 | MONITOR_FUEL_SYSTEM_B1 | Fuel System Monitor Bank 1 | +| 82 | MONITOR_FUEL_SYSTEM_B2 | Fuel System Monitor Bank 2 | +| 83 | MONITOR_FUEL_SYSTEM_B3 | Fuel System Monitor Bank 3 | +| 84 | MONITOR_FUEL_SYSTEM_B4 | Fuel System Monitor Bank 4 | +| 85 | MONITOR_BOOST_PRESSURE_B1 | Boost Pressure Control Monitor Bank 1 | +| 86 | MONITOR_BOOST_PRESSURE_B2 | Boost Pressure Control Monitor Bank 1 | +| *gap* | | | +| 90 | MONITOR_NOX_ABSORBER_B1 | NOx Absorber Monitor Bank 1 | +| 91 | MONITOR_NOX_ABSORBER_B2 | NOx Absorber Monitor Bank 2 | +| *gap* | | | +| 98 | MONITOR_NOX_CATALYST_B1 | NOx Catalyst Monitor Bank 1 | +| 99 | MONITOR_NOX_CATALYST_B2 | NOx Catalyst Monitor Bank 2 | +| *gap* | | | +| A0 | MIDS_F | Supported MIDs [A1-C0] | +| A1 | MONITOR_MISFIRE_GENERAL | Misfire Monitor General Data | +| A2 | MONITOR_MISFIRE_CYLINDER_1 | Misfire Cylinder 1 Data | +| A3 | MONITOR_MISFIRE_CYLINDER_2 | Misfire Cylinder 2 Data | +| A4 | MONITOR_MISFIRE_CYLINDER_3 | Misfire Cylinder 3 Data | +| A5 | MONITOR_MISFIRE_CYLINDER_4 | Misfire Cylinder 4 Data | +| A6 | MONITOR_MISFIRE_CYLINDER_5 | Misfire Cylinder 5 Data | +| A7 | MONITOR_MISFIRE_CYLINDER_6 | Misfire Cylinder 6 Data | +| A8 | MONITOR_MISFIRE_CYLINDER_7 | Misfire Cylinder 7 Data | +| A9 | MONITOR_MISFIRE_CYLINDER_8 | Misfire Cylinder 8 Data | +| AA | MONITOR_MISFIRE_CYLINDER_9 | Misfire Cylinder 9 Data | +| AB | MONITOR_MISFIRE_CYLINDER_10 | Misfire Cylinder 10 Data | +| AC | MONITOR_MISFIRE_CYLINDER_11 | Misfire Cylinder 11 Data | +| AD | MONITOR_MISFIRE_CYLINDER_12 | Misfire Cylinder 12 Data | +| *gap* | | | +| B0 | MONITOR_PM_FILTER_B1 | PM Filter Monitor Bank 1 | +| B1 | MONITOR_PM_FILTER_B2 | PM Filter Monitor Bank 2 | + +
+ # Mode 07 The return value will be encoded in the same structure as the Mode 03 `GET_DTC` command. -|PID | Name | Description | -|-----|----------------|------------------------------| -| N/A | GET_FREEZE_DTC | Get Freeze DTCs | +|PID | Name | Description | +|-----|-----------------|----------------------------------------------| +| N/A | GET_CURRENT_DTC | Get DTCs from the current/last driving cycle |
From dd6dbd84f871f2e79e058afdf38757c9441fcac4 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 5 Jul 2016 15:30:46 -0400 Subject: [PATCH 421/569] added response value column to commands docs --- docs/Commands.md | 196 +++++++++++++++++++++++------------------------ 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/docs/Commands.md b/docs/Commands.md index 232b99b2..2c029e88 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -72,104 +72,104 @@ obd.commands.has_pid(1, 12) # True # Mode 01 -|PID | Name | Description | -|----|---------------------------|-----------------------------------------| -| 00 | PIDS_A | Supported PIDs [01-20] | -| 01 | STATUS | Status since DTCs cleared | -| 02 | FREEZE_DTC | DTC that triggered the freeze frame | -| 03 | FUEL_STATUS | Fuel System Status | -| 04 | ENGINE_LOAD | Calculated Engine Load | -| 05 | COOLANT_TEMP | Engine Coolant Temperature | -| 06 | SHORT_FUEL_TRIM_1 | Short Term Fuel Trim - Bank 1 | -| 07 | LONG_FUEL_TRIM_1 | Long Term Fuel Trim - Bank 1 | -| 08 | SHORT_FUEL_TRIM_2 | Short Term Fuel Trim - Bank 2 | -| 09 | LONG_FUEL_TRIM_2 | Long Term Fuel Trim - Bank 2 | -| 0A | FUEL_PRESSURE | Fuel Pressure | -| 0B | INTAKE_PRESSURE | Intake Manifold Pressure | -| 0C | RPM | Engine RPM | -| 0D | SPEED | Vehicle Speed | -| 0E | TIMING_ADVANCE | Timing Advance | -| 0F | INTAKE_TEMP | Intake Air Temp | -| 10 | MAF | Air Flow Rate (MAF) | -| 11 | THROTTLE_POS | Throttle Position | -| 12 | AIR_STATUS | Secondary Air Status | -| 13 | O2_SENSORS | O2 Sensors Present | -| 14 | O2_B1S1 | O2: Bank 1 - Sensor 1 Voltage | -| 15 | O2_B1S2 | O2: Bank 1 - Sensor 2 Voltage | -| 16 | O2_B1S3 | O2: Bank 1 - Sensor 3 Voltage | -| 17 | O2_B1S4 | O2: Bank 1 - Sensor 4 Voltage | -| 18 | O2_B2S1 | O2: Bank 2 - Sensor 1 Voltage | -| 19 | O2_B2S2 | O2: Bank 2 - Sensor 2 Voltage | -| 1A | O2_B2S3 | O2: Bank 2 - Sensor 3 Voltage | -| 1B | O2_B2S4 | O2: Bank 2 - Sensor 4 Voltage | -| 1C | OBD_COMPLIANCE | OBD Standards Compliance | -| 1D | O2_SENSORS_ALT | O2 Sensors Present (alternate) | -| 1E | AUX_INPUT_STATUS | Auxiliary input status (power take off) | -| 1F | RUN_TIME | Engine Run Time | -| 20 | PIDS_B | Supported PIDs [21-40] | -| 21 | DISTANCE_W_MIL | Distance Traveled with MIL on | -| 22 | FUEL_RAIL_PRESSURE_VAC | Fuel Rail Pressure (relative to vacuum) | -| 23 | FUEL_RAIL_PRESSURE_DIRECT | Fuel Rail Pressure (direct inject) | -| 24 | O2_S1_WR_VOLTAGE | 02 Sensor 1 WR Lambda Voltage | -| 25 | O2_S2_WR_VOLTAGE | 02 Sensor 2 WR Lambda Voltage | -| 26 | O2_S3_WR_VOLTAGE | 02 Sensor 3 WR Lambda Voltage | -| 27 | O2_S4_WR_VOLTAGE | 02 Sensor 4 WR Lambda Voltage | -| 28 | O2_S5_WR_VOLTAGE | 02 Sensor 5 WR Lambda Voltage | -| 29 | O2_S6_WR_VOLTAGE | 02 Sensor 6 WR Lambda Voltage | -| 2A | O2_S7_WR_VOLTAGE | 02 Sensor 7 WR Lambda Voltage | -| 2B | O2_S8_WR_VOLTAGE | 02 Sensor 8 WR Lambda Voltage | -| 2C | COMMANDED_EGR | Commanded EGR | -| 2D | EGR_ERROR | EGR Error | -| 2E | EVAPORATIVE_PURGE | Commanded Evaporative Purge | -| 2F | FUEL_LEVEL | Fuel Level Input | -| 30 | WARMUPS_SINCE_DTC_CLEAR | Number of warm-ups since codes cleared | -| 31 | DISTANCE_SINCE_DTC_CLEAR | Distance traveled since codes cleared | -| 32 | EVAP_VAPOR_PRESSURE | Evaporative system vapor pressure | -| 33 | BAROMETRIC_PRESSURE | Barometric Pressure | -| 34 | O2_S1_WR_CURRENT | 02 Sensor 1 WR Lambda Current | -| 35 | O2_S2_WR_CURRENT | 02 Sensor 2 WR Lambda Current | -| 36 | O2_S3_WR_CURRENT | 02 Sensor 3 WR Lambda Current | -| 37 | O2_S4_WR_CURRENT | 02 Sensor 4 WR Lambda Current | -| 38 | O2_S5_WR_CURRENT | 02 Sensor 5 WR Lambda Current | -| 39 | O2_S6_WR_CURRENT | 02 Sensor 6 WR Lambda Current | -| 3A | O2_S7_WR_CURRENT | 02 Sensor 7 WR Lambda Current | -| 3B | O2_S8_WR_CURRENT | 02 Sensor 8 WR Lambda Current | -| 3C | CATALYST_TEMP_B1S1 | Catalyst Temperature: Bank 1 - Sensor 1 | -| 3D | CATALYST_TEMP_B2S1 | Catalyst Temperature: Bank 2 - Sensor 1 | -| 3E | CATALYST_TEMP_B1S2 | Catalyst Temperature: Bank 1 - Sensor 2 | -| 3F | CATALYST_TEMP_B2S2 | Catalyst Temperature: Bank 2 - Sensor 2 | -| 40 | PIDS_C | Supported PIDs [41-60] | -| 41 | *unsupported* | *unsupported* | -| 42 | *unsupported* | *unsupported* | -| 43 | *unsupported* | *unsupported* | -| 44 | *unsupported* | *unsupported* | -| 45 | RELATIVE_THROTTLE_POS | Relative throttle position | -| 46 | AMBIANT_AIR_TEMP | Ambient air temperature | -| 47 | THROTTLE_POS_B | Absolute throttle position B | -| 48 | THROTTLE_POS_C | Absolute throttle position C | -| 49 | ACCELERATOR_POS_D | Accelerator pedal position D | -| 4A | ACCELERATOR_POS_E | Accelerator pedal position E | -| 4B | ACCELERATOR_POS_F | Accelerator pedal position F | -| 4C | THROTTLE_ACTUATOR | Commanded throttle actuator | -| 4D | RUN_TIME_MIL | Time run with MIL on | -| 4E | TIME_SINCE_DTC_CLEARED | Time since trouble codes cleared | -| 4F | *unsupported* | *unsupported* | -| 50 | MAX_MAF | Maximum value for mass air flow sensor | -| 51 | FUEL_TYPE | Fuel Type | -| 52 | ETHANOL_PERCENT | Ethanol Fuel Percent | -| 53 | EVAP_VAPOR_PRESSURE_ABS | Absolute Evap system Vapor Pressure | -| 54 | EVAP_VAPOR_PRESSURE_ALT | Evap system vapor pressure | -| 55 | SHORT_O2_TRIM_B1 | Short term secondary O2 trim - Bank 1 | -| 56 | LONG_O2_TRIM_B1 | Long term secondary O2 trim - Bank 1 | -| 57 | SHORT_O2_TRIM_B2 | Short term secondary O2 trim - Bank 2 | -| 58 | LONG_O2_TRIM_B2 | Long term secondary O2 trim - Bank 2 | -| 59 | FUEL_RAIL_PRESSURE_ABS | Fuel rail pressure (absolute) | -| 5A | RELATIVE_ACCEL_POS | Relative accelerator pedal position | -| 5B | HYBRID_BATTERY_REMAINING | Hybrid battery pack remaining life | -| 5C | OIL_TEMP | Engine oil temperature | -| 5D | FUEL_INJECT_TIMING | Fuel injection timing | -| 5E | FUEL_RATE | Engine fuel rate | -| 5F | *unsupported* | *unsupported* | +|PID | Name | Description | Response Value | +|----|---------------------------|-----------------------------------------|-----------------------| +| 00 | PIDS_A | Supported PIDs [01-20] | bitstring | +| 01 | STATUS | Status since DTCs cleared | | +| 02 | FREEZE_DTC | DTC that triggered the freeze frame | | +| 03 | FUEL_STATUS | Fuel System Status | string | +| 04 | ENGINE_LOAD | Calculated Engine Load | Unit.percent | +| 05 | COOLANT_TEMP | Engine Coolant Temperature | Unit.celsius | +| 06 | SHORT_FUEL_TRIM_1 | Short Term Fuel Trim - Bank 1 | Unit.percent | +| 07 | LONG_FUEL_TRIM_1 | Long Term Fuel Trim - Bank 1 | Unit.percent | +| 08 | SHORT_FUEL_TRIM_2 | Short Term Fuel Trim - Bank 2 | Unit.percent | +| 09 | LONG_FUEL_TRIM_2 | Long Term Fuel Trim - Bank 2 | Unit.percent | +| 0A | FUEL_PRESSURE | Fuel Pressure | Unit.kilopascal | +| 0B | INTAKE_PRESSURE | Intake Manifold Pressure | Unit.kilopascal | +| 0C | RPM | Engine RPM | Unit.rpm | +| 0D | SPEED | Vehicle Speed | Unit.kph | +| 0E | TIMING_ADVANCE | Timing Advance | Unit.degree | +| 0F | INTAKE_TEMP | Intake Air Temp | Unit.celsius | +| 10 | MAF | Air Flow Rate (MAF) | Unit.grams_per_second | +| 11 | THROTTLE_POS | Throttle Position | Unit.percent | +| 12 | AIR_STATUS | Secondary Air Status | string | +| 13 | O2_SENSORS | O2 Sensors Present | | +| 14 | O2_B1S1 | O2: Bank 1 - Sensor 1 Voltage | Unit.volt | +| 15 | O2_B1S2 | O2: Bank 1 - Sensor 2 Voltage | Unit.volt | +| 16 | O2_B1S3 | O2: Bank 1 - Sensor 3 Voltage | Unit.volt | +| 17 | O2_B1S4 | O2: Bank 1 - Sensor 4 Voltage | Unit.volt | +| 18 | O2_B2S1 | O2: Bank 2 - Sensor 1 Voltage | Unit.volt | +| 19 | O2_B2S2 | O2: Bank 2 - Sensor 2 Voltage | Unit.volt | +| 1A | O2_B2S3 | O2: Bank 2 - Sensor 3 Voltage | Unit.volt | +| 1B | O2_B2S4 | O2: Bank 2 - Sensor 4 Voltage | Unit.volt | +| 1C | OBD_COMPLIANCE | OBD Standards Compliance | string | +| 1D | O2_SENSORS_ALT | O2 Sensors Present (alternate) | | +| 1E | AUX_INPUT_STATUS | Auxiliary input status (power take off) | boolean | +| 1F | RUN_TIME | Engine Run Time | Unit.second | +| 20 | PIDS_B | Supported PIDs [21-40] | bitstring | +| 21 | DISTANCE_W_MIL | Distance Traveled with MIL on | Unit.kilometer | +| 22 | FUEL_RAIL_PRESSURE_VAC | Fuel Rail Pressure (relative to vacuum) | Unit.kilopascal | +| 23 | FUEL_RAIL_PRESSURE_DIRECT | Fuel Rail Pressure (direct inject) | Unit.kilopascal | +| 24 | O2_S1_WR_VOLTAGE | 02 Sensor 1 WR Lambda Voltage | Unit.volt | +| 25 | O2_S2_WR_VOLTAGE | 02 Sensor 2 WR Lambda Voltage | Unit.volt | +| 26 | O2_S3_WR_VOLTAGE | 02 Sensor 3 WR Lambda Voltage | Unit.volt | +| 27 | O2_S4_WR_VOLTAGE | 02 Sensor 4 WR Lambda Voltage | Unit.volt | +| 28 | O2_S5_WR_VOLTAGE | 02 Sensor 5 WR Lambda Voltage | Unit.volt | +| 29 | O2_S6_WR_VOLTAGE | 02 Sensor 6 WR Lambda Voltage | Unit.volt | +| 2A | O2_S7_WR_VOLTAGE | 02 Sensor 7 WR Lambda Voltage | Unit.volt | +| 2B | O2_S8_WR_VOLTAGE | 02 Sensor 8 WR Lambda Voltage | Unit.volt | +| 2C | COMMANDED_EGR | Commanded EGR | Unit.percent | +| 2D | EGR_ERROR | EGR Error | Unit.percent | +| 2E | EVAPORATIVE_PURGE | Commanded Evaporative Purge | Unit.percent | +| 2F | FUEL_LEVEL | Fuel Level Input | Unit.percent | +| 30 | WARMUPS_SINCE_DTC_CLEAR | Number of warm-ups since codes cleared | Unit.count | +| 31 | DISTANCE_SINCE_DTC_CLEAR | Distance traveled since codes cleared | Unit.kilometer | +| 32 | EVAP_VAPOR_PRESSURE | Evaporative system vapor pressure | Unit.pascal | +| 33 | BAROMETRIC_PRESSURE | Barometric Pressure | Unit.kilopascal | +| 34 | O2_S1_WR_CURRENT | 02 Sensor 1 WR Lambda Current | Unit.milliampere | +| 35 | O2_S2_WR_CURRENT | 02 Sensor 2 WR Lambda Current | Unit.milliampere | +| 36 | O2_S3_WR_CURRENT | 02 Sensor 3 WR Lambda Current | Unit.milliampere | +| 37 | O2_S4_WR_CURRENT | 02 Sensor 4 WR Lambda Current | Unit.milliampere | +| 38 | O2_S5_WR_CURRENT | 02 Sensor 5 WR Lambda Current | Unit.milliampere | +| 39 | O2_S6_WR_CURRENT | 02 Sensor 6 WR Lambda Current | Unit.milliampere | +| 3A | O2_S7_WR_CURRENT | 02 Sensor 7 WR Lambda Current | Unit.milliampere | +| 3B | O2_S8_WR_CURRENT | 02 Sensor 8 WR Lambda Current | Unit.milliampere | +| 3C | CATALYST_TEMP_B1S1 | Catalyst Temperature: Bank 1 - Sensor 1 | Unit.celsius | +| 3D | CATALYST_TEMP_B2S1 | Catalyst Temperature: Bank 2 - Sensor 1 | Unit.celsius | +| 3E | CATALYST_TEMP_B1S2 | Catalyst Temperature: Bank 1 - Sensor 2 | Unit.celsius | +| 3F | CATALYST_TEMP_B2S2 | Catalyst Temperature: Bank 2 - Sensor 2 | Unit.celsius | +| 40 | PIDS_C | Supported PIDs [41-60] | bitstring | +| 41 | *unsupported* | *unsupported* | | +| 42 | *unsupported* | *unsupported* | | +| 43 | *unsupported* | *unsupported* | | +| 44 | *unsupported* | *unsupported* | | +| 45 | RELATIVE_THROTTLE_POS | Relative throttle position | Unit.percent | +| 46 | AMBIANT_AIR_TEMP | Ambient air temperature | Unit.celsius | +| 47 | THROTTLE_POS_B | Absolute throttle position B | Unit.percent | +| 48 | THROTTLE_POS_C | Absolute throttle position C | Unit.percent | +| 49 | ACCELERATOR_POS_D | Accelerator pedal position D | Unit.percent | +| 4A | ACCELERATOR_POS_E | Accelerator pedal position E | Unit.percent | +| 4B | ACCELERATOR_POS_F | Accelerator pedal position F | Unit.percent | +| 4C | THROTTLE_ACTUATOR | Commanded throttle actuator | Unit.percent | +| 4D | RUN_TIME_MIL | Time run with MIL on | Unit.minute | +| 4E | TIME_SINCE_DTC_CLEARED | Time since trouble codes cleared | Unit.minute | +| 4F | *unsupported* | *unsupported* | | +| 50 | MAX_MAF | Maximum value for mass air flow sensor | Unit.grams_per_second | +| 51 | FUEL_TYPE | Fuel Type | string | +| 52 | ETHANOL_PERCENT | Ethanol Fuel Percent | Unit.percent | +| 53 | EVAP_VAPOR_PRESSURE_ABS | Absolute Evap system Vapor Pressure | Unit.kilopascal | +| 54 | EVAP_VAPOR_PRESSURE_ALT | Evap system vapor pressure | Unit.pascal | +| 55 | SHORT_O2_TRIM_B1 | Short term secondary O2 trim - Bank 1 | Unit.percent | +| 56 | LONG_O2_TRIM_B1 | Long term secondary O2 trim - Bank 1 | Unit.percent | +| 57 | SHORT_O2_TRIM_B2 | Short term secondary O2 trim - Bank 2 | Unit.percent | +| 58 | LONG_O2_TRIM_B2 | Long term secondary O2 trim - Bank 2 | Unit.percent | +| 59 | FUEL_RAIL_PRESSURE_ABS | Fuel rail pressure (absolute) | Unit.kilopascal | +| 5A | RELATIVE_ACCEL_POS | Relative accelerator pedal position | Unit.percent | +| 5B | HYBRID_BATTERY_REMAINING | Hybrid battery pack remaining life | Unit.percent | +| 5C | OIL_TEMP | Engine oil temperature | Unit.celsius | +| 5D | FUEL_INJECT_TIMING | Fuel injection timing | Unit.degree | +| 5E | FUEL_RATE | Engine fuel rate | Unit.liters_per_hour | +| 5F | *unsupported* | *unsupported* | |
From 1ea15ba80ca9835699faecd5a4e208ad25a761ad Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 5 Jul 2016 15:36:43 -0400 Subject: [PATCH 422/569] use empty tuple to bank-align O2 sensor presence --- obd/decoders.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/obd/decoders.py b/obd/decoders.py index 74a1c0bf..e95a02ab 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -202,6 +202,7 @@ def o2_sensors(messages): d = messages[0].data bitstring = bytes_to_bits(d) return ( + (), # bank 0 is invalid tuple([ b == "1" for b in bitstring[:4] ]), # bank 1 tuple([ b == "1" for b in bitstring[4:] ]), # bank 2 ) @@ -215,6 +216,7 @@ def o2_sensors_alt(messages): d = messages[0].data bitstring = bytes_to_bits(d) return ( + (), # bank 0 is invalid tuple([ b == "1" for b in bitstring[:2] ]), # bank 1 tuple([ b == "1" for b in bitstring[2:4] ]), # bank 2 tuple([ b == "1" for b in bitstring[4:6] ]), # bank 3 From 818dd65db0a08e9ebbc45892c97236e851fb30f3 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 5 Jul 2016 15:48:05 -0400 Subject: [PATCH 423/569] documented O2 sensor presence --- docs/Commands.md | 4 ++-- docs/Responses.md | 27 ++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/docs/Commands.md b/docs/Commands.md index 2c029e88..88891a48 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -93,7 +93,7 @@ obd.commands.has_pid(1, 12) # True | 10 | MAF | Air Flow Rate (MAF) | Unit.grams_per_second | | 11 | THROTTLE_POS | Throttle Position | Unit.percent | | 12 | AIR_STATUS | Secondary Air Status | string | -| 13 | O2_SENSORS | O2 Sensors Present | | +| 13 | O2_SENSORS | O2 Sensors Present | [special](Responses.md#oxygen-sensors-present) | | 14 | O2_B1S1 | O2: Bank 1 - Sensor 1 Voltage | Unit.volt | | 15 | O2_B1S2 | O2: Bank 1 - Sensor 2 Voltage | Unit.volt | | 16 | O2_B1S3 | O2: Bank 1 - Sensor 3 Voltage | Unit.volt | @@ -103,7 +103,7 @@ obd.commands.has_pid(1, 12) # True | 1A | O2_B2S3 | O2: Bank 2 - Sensor 3 Voltage | Unit.volt | | 1B | O2_B2S4 | O2: Bank 2 - Sensor 4 Voltage | Unit.volt | | 1C | OBD_COMPLIANCE | OBD Standards Compliance | string | -| 1D | O2_SENSORS_ALT | O2 Sensors Present (alternate) | | +| 1D | O2_SENSORS_ALT | O2 Sensors Present (alternate) | [special](Responses.md#oxygen-sensors-present) | | 1E | AUX_INPUT_STATUS | Auxiliary input status (power take off) | boolean | | 1F | RUN_TIME | Engine Run Time | Unit.second | | 20 | PIDS_B | Supported PIDs [21-40] | bitstring | diff --git a/docs/Responses.md b/docs/Responses.md index 1cb292cd..5580b84c 100644 --- a/docs/Responses.md +++ b/docs/Responses.md @@ -25,7 +25,7 @@ if not r.is_null(): --- -# Values +# Pint Values The `value` property typically contains a [Pint](http://pint.readthedocs.io/en/latest/) `Quantity` object, but can also hold complex structures (depending on the request). Pint quantities combine a value and unit into a single class, and are used to represent physical values (such as "4 seconds", and "88 mph"). This allows for consistency when doing math and unit conversions. Pint maintains a registry of units, which is exposed in python-OBD as `obd.Unit`. @@ -69,4 +69,29 @@ import obd --- + +# Oxygen Sensors Present + +Returns a 2D structure of tuples (representing bank and sensor number), that holds boolean values for sensor presence. + +```python +# obd.commands.O2_SENSORS +responce.value = ( + (), # bank 0 is invalid, this is merely for correct indexing + (True, True, True, False), # bank 1 + (False, False, False, False) # bank 2 +) + +# obd.commands.O2_SENSORS_ALT +responce.value = ( + (), # bank 0 is invalid, this is merely for correct indexing + (True, True), # bank 1 + (True, False), # bank 2 + (False, False), # bank 2 + (False, False) # bank 2 +) +``` + +--- +
From e5bbb672d18f87cb897ed79ae13623bd5cb1b965 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 5 Jul 2016 15:59:38 -0400 Subject: [PATCH 424/569] return full DTC tuple in FREEZE_DTC, use "" for unknown DTC, fixed tests --- obd/commands.py | 2 +- obd/decoders.py | 11 +++-------- tests/test_decoders.py | 24 ++++++++++++------------ 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index a416d3ca..dbab7605 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -367,7 +367,7 @@ def base_commands(self): self.MIDS_A, self.GET_DTC, self.CLEAR_DTC, - self.GET_FREEZE_DTC, + self.GET_CURRENT_DTC, self.ELM_VERSION, self.ELM_VOLTAGE, ] diff --git a/obd/decoders.py b/obd/decoders.py index e95a02ab..3e19b583 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -377,7 +377,8 @@ def single_dtc(_bytes): dtc += str( (_bytes[0] >> 4) & 0b0011 ) # the next pair of 2 bits. Mask off the bits we read above dtc += bytes_to_hex(_bytes)[1:4] - return dtc + # pull a description if we have one + return (dtc, DTC.get(dtc, "")) def dtc(messages): @@ -395,13 +396,7 @@ def dtc(messages): dtc = single_dtc( (d[n-1], d[n]) ) if dtc is not None: - # pull a description if we have one - if dtc in DTC: - desc = DTC[dtc] - else: - desc = "Unknown error code" - - codes.append( (dtc, desc) ) + codes.append(dtc) return codes diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 66b7dcc2..9f36ce53 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -125,16 +125,16 @@ def test_air_status(): assert d.air_status(m("03")) == None def test_o2_sensors(): - assert d.o2_sensors(m("00")) == ((False, False, False, False), (False, False, False, False)) - assert d.o2_sensors(m("01")) == ((False, False, False, False), (False, False, False, True)) - assert d.o2_sensors(m("0F")) == ((False, False, False, False), (True, True, True, True)) - assert d.o2_sensors(m("F0")) == ((True, True, True, True), (False, False, False, False)) + assert d.o2_sensors(m("00")) == ((),(False, False, False, False), (False, False, False, False)) + assert d.o2_sensors(m("01")) == ((),(False, False, False, False), (False, False, False, True)) + assert d.o2_sensors(m("0F")) == ((),(False, False, False, False), (True, True, True, True)) + assert d.o2_sensors(m("F0")) == ((),(True, True, True, True), (False, False, False, False)) def test_o2_sensors_alt(): - assert d.o2_sensors_alt(m("00")) == ((False, False), (False, False), (False, False), (False, False)) - assert d.o2_sensors_alt(m("01")) == ((False, False), (False, False), (False, False), (False, True)) - assert d.o2_sensors_alt(m("0F")) == ((False, False), (False, False), (True, True), (True, True)) - assert d.o2_sensors_alt(m("F0")) == ((True, True), (True, True), (False, False), (False, False)) + assert d.o2_sensors_alt(m("00")) == ((),(False, False), (False, False), (False, False), (False, False)) + assert d.o2_sensors_alt(m("01")) == ((),(False, False), (False, False), (False, False), (False, True)) + assert d.o2_sensors_alt(m("0F")) == ((),(False, False), (False, False), (True, True), (True, True)) + assert d.o2_sensors_alt(m("F0")) == ((),(True, True), (True, True), (False, False), (False, False)) def test_aux_input_status(): assert d.aux_input_status(m("00")) == False @@ -154,14 +154,14 @@ def test_dtc(): # multiple codes assert d.dtc(m("010480034123")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), - ("B0003", "Unknown error code"), - ("C0123", "Unknown error code"), + ("B0003", ""), # unknown error codes return empty strings + ("C0123", ""), ] # invalid code lengths are dropped assert d.dtc(m("0104800341")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), - ("B0003", "Unknown error code"), + ("B0003", ""), ] # 0000 codes are dropped @@ -172,7 +172,7 @@ def test_dtc(): # test multiple messages assert d.dtc(m("0104") + m("8003") + m("0000")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), - ("B0003", "Unknown error code"), + ("B0003", ""), ] def test_monitor(): From 9810b4d0d946aef6ef7ae04e680cd7c4be8ca5d4 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 5 Jul 2016 16:12:31 -0400 Subject: [PATCH 425/569] fixed single_dtc decoder --- obd/decoders.py | 10 ++++++++-- tests/test_decoders.py | 6 ++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 3e19b583..a0501cd6 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -359,7 +359,7 @@ def fuel_type(_hex): return v -def single_dtc(_bytes): +def parse_dtc(_bytes): """ converts 2 bytes into a DTC code """ # check validity (also ignores padding that the ELM returns) @@ -381,6 +381,12 @@ def single_dtc(_bytes): return (dtc, DTC.get(dtc, "")) +def single_dtc(messages): + """ parses a single DTC from a message """ + d = messages[0].data + return parse_dtc(d) + + def dtc(messages): """ converts a frame of 2-byte DTCs into a list of DTCs """ codes = [] @@ -393,7 +399,7 @@ def dtc(messages): for n in range(1, len(d), 2): # parse the code - dtc = single_dtc( (d[n-1], d[n]) ) + dtc = parse_dtc( (d[n-1], d[n]) ) if dtc is not None: codes.append(dtc) diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 9f36ce53..1735391d 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -146,6 +146,12 @@ def test_elm_voltage(): assert d.elm_voltage([ Message([ Frame("12") ]) ]) == 12 * Unit.volt assert d.elm_voltage([ Message([ Frame("12ABCD") ]) ]) == None +def test_single_dtc(): + assert d.single_dtc(m("0104")) == ("P0104", "Mass or Volume Air Flow Circuit Intermittent") + assert d.single_dtc(m("4123")) == ("C0123", "") + assert d.single_dtc(m("01")) == None + assert d.single_dtc(m("010400")) == None + def test_dtc(): assert d.dtc(m("0104")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), From f4788a6c7d0ca9092ddc2412aeb9ccf2a356aa91 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 5 Jul 2016 16:36:52 -0400 Subject: [PATCH 426/569] adding more links and response value descriptions --- docs/Commands.md | 230 +++++++++++++++++++++------------------------- docs/Responses.md | 26 ++++++ 2 files changed, 133 insertions(+), 123 deletions(-) diff --git a/docs/Commands.md b/docs/Commands.md index 88891a48..bd39dd4f 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -63,10 +63,10 @@ obd.commands.has_pid(1, 12) # True # OBD-II adapter (ELM327 commands) -|PID | Name | Description | -|-----|-------------|-----------------------------------------| -| N/A | ELM_VERSION | OBD-II adapter version string | -| N/A | ELM_VOLTAGE | Voltage detected by OBD-II adapter | +|PID | Name | Description | Response Value | +|-----|-------------|-----------------------------------------|-----------------------| +| N/A | ELM_VERSION | OBD-II adapter version string | string | +| N/A | ELM_VOLTAGE | Voltage detected by OBD-II adapter | Unit.volt |
@@ -76,7 +76,7 @@ obd.commands.has_pid(1, 12) # True |----|---------------------------|-----------------------------------------|-----------------------| | 00 | PIDS_A | Supported PIDs [01-20] | bitstring | | 01 | STATUS | Status since DTCs cleared | | -| 02 | FREEZE_DTC | DTC that triggered the freeze frame | | +| 02 | FREEZE_DTC | DTC that triggered the freeze frame | [special](Responses.md#diagnostic-trouble-codes-dtcs) | | 03 | FUEL_STATUS | Fuel System Status | string | | 04 | ENGINE_LOAD | Calculated Engine Load | Unit.percent | | 05 | COOLANT_TEMP | Engine Coolant Temperature | Unit.celsius | @@ -189,143 +189,127 @@ obd.commands.DTC_RPM # the Mode 02 command # Mode 03 -Mode 03 contains a single command `GET_DTC` which requests all diagnostic trouble codes from the vehicle's engine. +Mode 03 contains a single command `GET_DTC` which requests all diagnostic trouble codes from the vehicle. The response will contain the codes themselves, as well as a description (if python-OBD has one). See the [DTC Responses](Responses.md#diagnostic-trouble-codes-dtcs) section for more details. -|PID | Name | Description | -|-----|---------|-----------------------------------------| -| N/A | GET_DTC | Get Diagnostic Trouble Codes | +|PID | Name | Description | Response Value | +|-----|---------|-----------------------------------------|-----------------------| +| N/A | GET_DTC | Get Diagnostic Trouble Codes | [special](Responses.md#diagnostic-trouble-codes-dtcs) | -This command requests all diagnostic trouble codes from the vehicle's engine. The `value` field of the response object will contain a list of tuples, where each tuple contains the DTC, and a string description of that DTC (if available). - -```python -import obd -connection = obd.OBD() -r = connection.query(obd.commands.GET_DTC) -print(r.value) - -''' -example output: -[ - ("P0030", "HO2S Heater Control Circuit"), - ("P1367", "Unknown error code") -] -''' -```
# Mode 04 -|PID | Name | Description | -|-----|-----------|-----------------------------------------| -| N/A | CLEAR_DTC | Clear DTCs and Freeze data | +|PID | Name | Description | Response Value | +|-----|-----------|-----------------------------------------|-----------------------| +| N/A | CLEAR_DTC | Clear DTCs and Freeze data | N/A |
# Mode 06 -Mode 06 commands are used to monitor various test results from the vehicle. Currently, Mode 06 commands are only implemented for CAN protocols (ISO 15765-4). - -|PID | Name | Description | -|-------|-----------------------------|--------------------------------------------| -| 00 | MIDS_A | Supported MIDs [01-20] | -| 01 | MONITOR_O2_B1S1 | O2 Sensor Monitor Bank 1 - Sensor 1 | -| 02 | MONITOR_O2_B1S2 | O2 Sensor Monitor Bank 1 - Sensor 2 | -| 03 | MONITOR_O2_B1S3 | O2 Sensor Monitor Bank 1 - Sensor 3 | -| 04 | MONITOR_O2_B1S4 | O2 Sensor Monitor Bank 1 - Sensor 4 | -| 05 | MONITOR_O2_B2S1 | O2 Sensor Monitor Bank 2 - Sensor 1 | -| 06 | MONITOR_O2_B2S2 | O2 Sensor Monitor Bank 2 - Sensor 2 | -| 07 | MONITOR_O2_B2S3 | O2 Sensor Monitor Bank 2 - Sensor 3 | -| 08 | MONITOR_O2_B2S4 | O2 Sensor Monitor Bank 2 - Sensor 4 | -| 09 | MONITOR_O2_B3S1 | O2 Sensor Monitor Bank 3 - Sensor 1 | -| 0A | MONITOR_O2_B3S2 | O2 Sensor Monitor Bank 3 - Sensor 2 | -| 0B | MONITOR_O2_B3S3 | O2 Sensor Monitor Bank 3 - Sensor 3 | -| 0C | MONITOR_O2_B3S4 | O2 Sensor Monitor Bank 3 - Sensor 4 | -| 0D | MONITOR_O2_B4S1 | O2 Sensor Monitor Bank 4 - Sensor 1 | -| 0E | MONITOR_O2_B4S2 | O2 Sensor Monitor Bank 4 - Sensor 2 | -| 0F | MONITOR_O2_B4S3 | O2 Sensor Monitor Bank 4 - Sensor 3 | -| 10 | MONITOR_O2_B4S4 | O2 Sensor Monitor Bank 4 - Sensor 4 | +Mode 06 commands are used to monitor various test results from the vehicle. All commands in this mode return the same datatype, as described in the [Monitor Response](Responses.md#monitors-mode-06-responses) section. Currently, mode 06 commands are only implemented for CAN protocols (ISO 15765-4). + +|PID | Name | Description | Response Value | +|-------|-----------------------------|--------------------------------------------|-----------------------| +| 00 | MIDS_A | Supported MIDs [01-20] | bitstring | +| 01 | MONITOR_O2_B1S1 | O2 Sensor Monitor Bank 1 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 02 | MONITOR_O2_B1S2 | O2 Sensor Monitor Bank 1 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) | +| 03 | MONITOR_O2_B1S3 | O2 Sensor Monitor Bank 1 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) | +| 04 | MONITOR_O2_B1S4 | O2 Sensor Monitor Bank 1 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) | +| 05 | MONITOR_O2_B2S1 | O2 Sensor Monitor Bank 2 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 06 | MONITOR_O2_B2S2 | O2 Sensor Monitor Bank 2 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) | +| 07 | MONITOR_O2_B2S3 | O2 Sensor Monitor Bank 2 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) | +| 08 | MONITOR_O2_B2S4 | O2 Sensor Monitor Bank 2 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) | +| 09 | MONITOR_O2_B3S1 | O2 Sensor Monitor Bank 3 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 0A | MONITOR_O2_B3S2 | O2 Sensor Monitor Bank 3 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) | +| 0B | MONITOR_O2_B3S3 | O2 Sensor Monitor Bank 3 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) | +| 0C | MONITOR_O2_B3S4 | O2 Sensor Monitor Bank 3 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) | +| 0D | MONITOR_O2_B4S1 | O2 Sensor Monitor Bank 4 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 0E | MONITOR_O2_B4S2 | O2 Sensor Monitor Bank 4 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) | +| 0F | MONITOR_O2_B4S3 | O2 Sensor Monitor Bank 4 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) | +| 10 | MONITOR_O2_B4S4 | O2 Sensor Monitor Bank 4 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| 20 | MIDS_B | Supported MIDs [21-40] | -| 21 | MONITOR_CATALYST_B1 | Catalyst Monitor Bank 1 | -| 22 | MONITOR_CATALYST_B2 | Catalyst Monitor Bank 2 | -| 23 | MONITOR_CATALYST_B3 | Catalyst Monitor Bank 3 | -| 24 | MONITOR_CATALYST_B4 | Catalyst Monitor Bank 4 | +| 20 | MIDS_B | Supported MIDs [21-40] | bitstring | +| 21 | MONITOR_CATALYST_B1 | Catalyst Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 22 | MONITOR_CATALYST_B2 | Catalyst Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) | +| 23 | MONITOR_CATALYST_B3 | Catalyst Monitor Bank 3 | [monitor](Responses.md#monitors-mode-06-responses) | +| 24 | MONITOR_CATALYST_B4 | Catalyst Monitor Bank 4 | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| 31 | MONITOR_EGR_B1 | EGR Monitor Bank 1 | -| 32 | MONITOR_EGR_B2 | EGR Monitor Bank 2 | -| 33 | MONITOR_EGR_B3 | EGR Monitor Bank 3 | -| 34 | MONITOR_EGR_B4 | EGR Monitor Bank 4 | -| 35 | MONITOR_VVT_B1 | VVT Monitor Bank 1 | -| 36 | MONITOR_VVT_B2 | VVT Monitor Bank 2 | -| 37 | MONITOR_VVT_B3 | VVT Monitor Bank 3 | -| 38 | MONITOR_VVT_B4 | VVT Monitor Bank 4 | -| 39 | MONITOR_EVAP_150 | EVAP Monitor (Cap Off / 0.150\") | -| 3A | MONITOR_EVAP_090 | EVAP Monitor (0.090\") | -| 3B | MONITOR_EVAP_040 | EVAP Monitor (0.040\") | -| 3C | MONITOR_EVAP_020 | EVAP Monitor (0.020\") | -| 3D | MONITOR_PURGE_FLOW | Purge Flow Monitor | +| 31 | MONITOR_EGR_B1 | EGR Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 32 | MONITOR_EGR_B2 | EGR Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) | +| 33 | MONITOR_EGR_B3 | EGR Monitor Bank 3 | [monitor](Responses.md#monitors-mode-06-responses) | +| 34 | MONITOR_EGR_B4 | EGR Monitor Bank 4 | [monitor](Responses.md#monitors-mode-06-responses) | +| 35 | MONITOR_VVT_B1 | VVT Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 36 | MONITOR_VVT_B2 | VVT Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) | +| 37 | MONITOR_VVT_B3 | VVT Monitor Bank 3 | [monitor](Responses.md#monitors-mode-06-responses) | +| 38 | MONITOR_VVT_B4 | VVT Monitor Bank 4 | [monitor](Responses.md#monitors-mode-06-responses) | +| 39 | MONITOR_EVAP_150 | EVAP Monitor (Cap Off / 0.150\") | [monitor](Responses.md#monitors-mode-06-responses) | +| 3A | MONITOR_EVAP_090 | EVAP Monitor (0.090\") | [monitor](Responses.md#monitors-mode-06-responses) | +| 3B | MONITOR_EVAP_040 | EVAP Monitor (0.040\") | [monitor](Responses.md#monitors-mode-06-responses) | +| 3C | MONITOR_EVAP_020 | EVAP Monitor (0.020\") | [monitor](Responses.md#monitors-mode-06-responses) | +| 3D | MONITOR_PURGE_FLOW | Purge Flow Monitor | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| 40 | MIDS_C | Supported MIDs [41-60] | -| 41 | MONITOR_O2_HEATER_B1S1 | O2 Sensor Heater Monitor Bank 1 - Sensor 1 | -| 42 | MONITOR_O2_HEATER_B1S2 | O2 Sensor Heater Monitor Bank 1 - Sensor 2 | -| 43 | MONITOR_O2_HEATER_B1S3 | O2 Sensor Heater Monitor Bank 1 - Sensor 3 | -| 44 | MONITOR_O2_HEATER_B1S4 | O2 Sensor Heater Monitor Bank 1 - Sensor 4 | -| 45 | MONITOR_O2_HEATER_B2S1 | O2 Sensor Heater Monitor Bank 2 - Sensor 1 | -| 46 | MONITOR_O2_HEATER_B2S2 | O2 Sensor Heater Monitor Bank 2 - Sensor 2 | -| 47 | MONITOR_O2_HEATER_B2S3 | O2 Sensor Heater Monitor Bank 2 - Sensor 3 | -| 48 | MONITOR_O2_HEATER_B2S4 | O2 Sensor Heater Monitor Bank 2 - Sensor 4 | -| 49 | MONITOR_O2_HEATER_B3S1 | O2 Sensor Heater Monitor Bank 3 - Sensor 1 | -| 4A | MONITOR_O2_HEATER_B3S2 | O2 Sensor Heater Monitor Bank 3 - Sensor 2 | -| 4B | MONITOR_O2_HEATER_B3S3 | O2 Sensor Heater Monitor Bank 3 - Sensor 3 | -| 4C | MONITOR_O2_HEATER_B3S4 | O2 Sensor Heater Monitor Bank 3 - Sensor 4 | -| 4D | MONITOR_O2_HEATER_B4S1 | O2 Sensor Heater Monitor Bank 4 - Sensor 1 | -| 4E | MONITOR_O2_HEATER_B4S2 | O2 Sensor Heater Monitor Bank 4 - Sensor 2 | -| 4F | MONITOR_O2_HEATER_B4S3 | O2 Sensor Heater Monitor Bank 4 - Sensor 3 | -| 50 | MONITOR_O2_HEATER_B4S4 | O2 Sensor Heater Monitor Bank 4 - Sensor 4 | +| 40 | MIDS_C | Supported MIDs [41-60] | bitstring | +| 41 | MONITOR_O2_HEATER_B1S1 | O2 Sensor Heater Monitor Bank 1 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 42 | MONITOR_O2_HEATER_B1S2 | O2 Sensor Heater Monitor Bank 1 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) | +| 43 | MONITOR_O2_HEATER_B1S3 | O2 Sensor Heater Monitor Bank 1 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) | +| 44 | MONITOR_O2_HEATER_B1S4 | O2 Sensor Heater Monitor Bank 1 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) | +| 45 | MONITOR_O2_HEATER_B2S1 | O2 Sensor Heater Monitor Bank 2 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 46 | MONITOR_O2_HEATER_B2S2 | O2 Sensor Heater Monitor Bank 2 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) | +| 47 | MONITOR_O2_HEATER_B2S3 | O2 Sensor Heater Monitor Bank 2 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) | +| 48 | MONITOR_O2_HEATER_B2S4 | O2 Sensor Heater Monitor Bank 2 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) | +| 49 | MONITOR_O2_HEATER_B3S1 | O2 Sensor Heater Monitor Bank 3 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 4A | MONITOR_O2_HEATER_B3S2 | O2 Sensor Heater Monitor Bank 3 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) | +| 4B | MONITOR_O2_HEATER_B3S3 | O2 Sensor Heater Monitor Bank 3 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) | +| 4C | MONITOR_O2_HEATER_B3S4 | O2 Sensor Heater Monitor Bank 3 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) | +| 4D | MONITOR_O2_HEATER_B4S1 | O2 Sensor Heater Monitor Bank 4 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 4E | MONITOR_O2_HEATER_B4S2 | O2 Sensor Heater Monitor Bank 4 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) | +| 4F | MONITOR_O2_HEATER_B4S3 | O2 Sensor Heater Monitor Bank 4 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) | +| 50 | MONITOR_O2_HEATER_B4S4 | O2 Sensor Heater Monitor Bank 4 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| 60 | MIDS_D | Supported MIDs [61-80] | -| 61 | MONITOR_HEATED_CATALYST_B1 | Heated Catalyst Monitor Bank 1 | -| 62 | MONITOR_HEATED_CATALYST_B2 | Heated Catalyst Monitor Bank 2 | -| 63 | MONITOR_HEATED_CATALYST_B3 | Heated Catalyst Monitor Bank 3 | -| 64 | MONITOR_HEATED_CATALYST_B4 | Heated Catalyst Monitor Bank 4 | +| 60 | MIDS_D | Supported MIDs [61-80] | bitstring | +| 61 | MONITOR_HEATED_CATALYST_B1 | Heated Catalyst Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 62 | MONITOR_HEATED_CATALYST_B2 | Heated Catalyst Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) | +| 63 | MONITOR_HEATED_CATALYST_B3 | Heated Catalyst Monitor Bank 3 | [monitor](Responses.md#monitors-mode-06-responses) | +| 64 | MONITOR_HEATED_CATALYST_B4 | Heated Catalyst Monitor Bank 4 | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| 71 | MONITOR_SECONDARY_AIR_1 | Secondary Air Monitor 1 | -| 72 | MONITOR_SECONDARY_AIR_2 | Secondary Air Monitor 2 | -| 73 | MONITOR_SECONDARY_AIR_3 | Secondary Air Monitor 3 | -| 74 | MONITOR_SECONDARY_AIR_4 | Secondary Air Monitor 4 | +| 71 | MONITOR_SECONDARY_AIR_1 | Secondary Air Monitor 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 72 | MONITOR_SECONDARY_AIR_2 | Secondary Air Monitor 2 | [monitor](Responses.md#monitors-mode-06-responses) | +| 73 | MONITOR_SECONDARY_AIR_3 | Secondary Air Monitor 3 | [monitor](Responses.md#monitors-mode-06-responses) | +| 74 | MONITOR_SECONDARY_AIR_4 | Secondary Air Monitor 4 | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| 80 | MIDS_E | Supported MIDs [81-A0] | -| 81 | MONITOR_FUEL_SYSTEM_B1 | Fuel System Monitor Bank 1 | -| 82 | MONITOR_FUEL_SYSTEM_B2 | Fuel System Monitor Bank 2 | -| 83 | MONITOR_FUEL_SYSTEM_B3 | Fuel System Monitor Bank 3 | -| 84 | MONITOR_FUEL_SYSTEM_B4 | Fuel System Monitor Bank 4 | -| 85 | MONITOR_BOOST_PRESSURE_B1 | Boost Pressure Control Monitor Bank 1 | -| 86 | MONITOR_BOOST_PRESSURE_B2 | Boost Pressure Control Monitor Bank 1 | +| 80 | MIDS_E | Supported MIDs [81-A0] | bitstring | +| 81 | MONITOR_FUEL_SYSTEM_B1 | Fuel System Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 82 | MONITOR_FUEL_SYSTEM_B2 | Fuel System Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) | +| 83 | MONITOR_FUEL_SYSTEM_B3 | Fuel System Monitor Bank 3 | [monitor](Responses.md#monitors-mode-06-responses) | +| 84 | MONITOR_FUEL_SYSTEM_B4 | Fuel System Monitor Bank 4 | [monitor](Responses.md#monitors-mode-06-responses) | +| 85 | MONITOR_BOOST_PRESSURE_B1 | Boost Pressure Control Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 86 | MONITOR_BOOST_PRESSURE_B2 | Boost Pressure Control Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| 90 | MONITOR_NOX_ABSORBER_B1 | NOx Absorber Monitor Bank 1 | -| 91 | MONITOR_NOX_ABSORBER_B2 | NOx Absorber Monitor Bank 2 | +| 90 | MONITOR_NOX_ABSORBER_B1 | NOx Absorber Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 91 | MONITOR_NOX_ABSORBER_B2 | NOx Absorber Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| 98 | MONITOR_NOX_CATALYST_B1 | NOx Catalyst Monitor Bank 1 | -| 99 | MONITOR_NOX_CATALYST_B2 | NOx Catalyst Monitor Bank 2 | +| 98 | MONITOR_NOX_CATALYST_B1 | NOx Catalyst Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| 99 | MONITOR_NOX_CATALYST_B2 | NOx Catalyst Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| A0 | MIDS_F | Supported MIDs [A1-C0] | -| A1 | MONITOR_MISFIRE_GENERAL | Misfire Monitor General Data | -| A2 | MONITOR_MISFIRE_CYLINDER_1 | Misfire Cylinder 1 Data | -| A3 | MONITOR_MISFIRE_CYLINDER_2 | Misfire Cylinder 2 Data | -| A4 | MONITOR_MISFIRE_CYLINDER_3 | Misfire Cylinder 3 Data | -| A5 | MONITOR_MISFIRE_CYLINDER_4 | Misfire Cylinder 4 Data | -| A6 | MONITOR_MISFIRE_CYLINDER_5 | Misfire Cylinder 5 Data | -| A7 | MONITOR_MISFIRE_CYLINDER_6 | Misfire Cylinder 6 Data | -| A8 | MONITOR_MISFIRE_CYLINDER_7 | Misfire Cylinder 7 Data | -| A9 | MONITOR_MISFIRE_CYLINDER_8 | Misfire Cylinder 8 Data | -| AA | MONITOR_MISFIRE_CYLINDER_9 | Misfire Cylinder 9 Data | -| AB | MONITOR_MISFIRE_CYLINDER_10 | Misfire Cylinder 10 Data | -| AC | MONITOR_MISFIRE_CYLINDER_11 | Misfire Cylinder 11 Data | -| AD | MONITOR_MISFIRE_CYLINDER_12 | Misfire Cylinder 12 Data | +| A0 | MIDS_F | Supported MIDs [A1-C0] | bitstring | +| A1 | MONITOR_MISFIRE_GENERAL | Misfire Monitor General Data | [monitor](Responses.md#monitors-mode-06-responses) | +| A2 | MONITOR_MISFIRE_CYLINDER_1 | Misfire Cylinder 1 Data | [monitor](Responses.md#monitors-mode-06-responses) | +| A3 | MONITOR_MISFIRE_CYLINDER_2 | Misfire Cylinder 2 Data | [monitor](Responses.md#monitors-mode-06-responses) | +| A4 | MONITOR_MISFIRE_CYLINDER_3 | Misfire Cylinder 3 Data | [monitor](Responses.md#monitors-mode-06-responses) | +| A5 | MONITOR_MISFIRE_CYLINDER_4 | Misfire Cylinder 4 Data | [monitor](Responses.md#monitors-mode-06-responses) | +| A6 | MONITOR_MISFIRE_CYLINDER_5 | Misfire Cylinder 5 Data | [monitor](Responses.md#monitors-mode-06-responses) | +| A7 | MONITOR_MISFIRE_CYLINDER_6 | Misfire Cylinder 6 Data | [monitor](Responses.md#monitors-mode-06-responses) | +| A8 | MONITOR_MISFIRE_CYLINDER_7 | Misfire Cylinder 7 Data | [monitor](Responses.md#monitors-mode-06-responses) | +| A9 | MONITOR_MISFIRE_CYLINDER_8 | Misfire Cylinder 8 Data | [monitor](Responses.md#monitors-mode-06-responses) | +| AA | MONITOR_MISFIRE_CYLINDER_9 | Misfire Cylinder 9 Data | [monitor](Responses.md#monitors-mode-06-responses) | +| AB | MONITOR_MISFIRE_CYLINDER_10 | Misfire Cylinder 10 Data | [monitor](Responses.md#monitors-mode-06-responses) | +| AC | MONITOR_MISFIRE_CYLINDER_11 | Misfire Cylinder 11 Data | [monitor](Responses.md#monitors-mode-06-responses) | +| AD | MONITOR_MISFIRE_CYLINDER_12 | Misfire Cylinder 12 Data | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| B0 | MONITOR_PM_FILTER_B1 | PM Filter Monitor Bank 1 | -| B1 | MONITOR_PM_FILTER_B2 | PM Filter Monitor Bank 2 | +| B0 | MONITOR_PM_FILTER_B1 | PM Filter Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | +| B1 | MONITOR_PM_FILTER_B2 | PM Filter Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) |
@@ -333,8 +317,8 @@ Mode 06 commands are used to monitor various test results from the vehicle. Curr The return value will be encoded in the same structure as the Mode 03 `GET_DTC` command. -|PID | Name | Description | -|-----|-----------------|----------------------------------------------| -| N/A | GET_CURRENT_DTC | Get DTCs from the current/last driving cycle | +|PID | Name | Description | Response Value | +|-----|-----------------|----------------------------------------------|-----------------------| +| N/A | GET_CURRENT_DTC | Get DTCs from the current/last driving cycle | [special](Responses.md#diagnostic-trouble-codes-dtcs) |
diff --git a/docs/Responses.md b/docs/Responses.md index 5580b84c..f545568f 100644 --- a/docs/Responses.md +++ b/docs/Responses.md @@ -69,6 +69,23 @@ import obd --- +# Diagnostic Trouble Codes (DTCs) + +Each DTC is represented by a tuple containing the DTC code, and a description (if python-OBD has one). When multiple DTCs are returned, they are stored in a list. + +```python +# obd.commands.GET_DTC +responce.value = [ + ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), + ("B0003", ""), # unknown error code, it's probably vehicle-specific + ("C0123", "") +] + +# obd.commands.FREEZE_DTC +responce.value = ("P0104", "Mass or Volume Air Flow Circuit Intermittent") +``` + +--- # Oxygen Sensors Present @@ -91,6 +108,15 @@ responce.value = ( (False, False) # bank 2 ) ``` +--- + +# Monitors (Mode 06 Responses) + +All mode 06 commands return `Monitor` objects holding various test results for the requested sensor. A single monitor response can hold multiple tests. + +```python +# TODO +``` --- From 5de345ab9520528a7662a185e9b414e9f4608e5a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 5 Jul 2016 16:45:27 -0400 Subject: [PATCH 427/569] added usage example to O2 sensors present --- docs/Responses.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/Responses.md b/docs/Responses.md index f545568f..c7fa52d1 100644 --- a/docs/Responses.md +++ b/docs/Responses.md @@ -107,6 +107,9 @@ responce.value = ( (False, False), # bank 2 (False, False) # bank 2 ) + +# example usage: +response.value[1][2] == True # Bank 1, Sensor 2 is present ``` --- From 52ab6b8ee25d973e331f50b6179e0bf89e66c5d5 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 5 Jul 2016 17:02:34 -0400 Subject: [PATCH 428/569] added module layout, updated basic usage code --- README.md | 3 ++- docs/index.md | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c6fe81a8..11862c8c 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ cmd = obd.commands.RPM # select an OBD command (sensor) response = connection.query(cmd) # send the command, and parse the response -print(response.value) +print(response.value) # returns unit-bearing values thanks to Pint +print(response.value.magnitude) # or simple floats ``` Documentation diff --git a/docs/index.md b/docs/index.md index 05282dce..2b1583f6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,14 +31,32 @@ cmd = obd.commands.RPM # select an OBD command (sensor) response = connection.query(cmd) # send the command, and parse the response -print(response.value) -print(response.unit) +print(response.value) # returns unit-bearing values thanks to Pint +print(response.value.magnitude) # or simple floats ``` OBD connections operate in a request-reply fashion. To retrieve data from the car, you must send commands that query for the data you want (e.g. RPM, Vehicle speed, etc). In python-OBD, this is done with the `query()` function. The commands themselves are represented as objects, and can be looked up by name or value in `obd.commands`. The `query()` function will return a response object with parsed data in its `value` and `unit` properties.
+# Module Layout + +```python +import obd + +obd.OBD # main OBD connection class +obd.Async # asynchronous OBD connection class +obd.commands # command tables +obd.Unit # unit tables (a Pint UnitRegistry) +obd.logger # the OBD module's root logger (for debug) +obd.OBDStatus # enum for connection status +obd.scan_serial # util function for manually scanning for OBD adapters +obd.OBDCommand # class for making your own OBD Commands +obd.ECU # enum for marking which ECU a command should listen to +``` + +
+ # License GNU General Public License V2 From 0ef0ee4a5e0c9d8418f7808a340a48cf810719f9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 5 Jul 2016 17:05:07 -0400 Subject: [PATCH 429/569] removed old set_supported() function --- obd/commands.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index dbab7605..86dcf94e 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -381,15 +381,6 @@ def pid_getters(self): return getters - def set_supported(self, mode, pid, v): - """ sets the boolean supported flag for the given command """ - if isinstance(v, bool): - if self.has(mode, pid): - self.modes[mode][pid].supported = v - else: - logger.warning("set_supported() only accepts boolean values") - - def has_command(self, c): """ checks for existance of a command by OBDCommand object """ return c in self.__dict__.values() From 55eaad095731b60c4127c728a68d521f0e21787e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 5 Jul 2016 18:26:08 -0400 Subject: [PATCH 430/569] upcase TID names, lookup tests by string name --- obd/OBDResponse.py | 15 +++++++++++++-- obd/codes.py | 24 ++++++++++++------------ tests/test_decoders.py | 8 ++++---- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py index 9619a3e6..f236699b 100644 --- a/obd/OBDResponse.py +++ b/obd/OBDResponse.py @@ -33,6 +33,11 @@ import time from .codes import * +import logging + +logger = logging.getLogger(__name__) + + class OBDResponse(): """ Standard response object for any OBDCommand """ @@ -119,8 +124,14 @@ def __str__(self): def __len__(self): return len(self.tests) - def __getitem__(self, tid): - return self._tests.get(tid, MonitorTest()) + def __getitem__(self, key): + if isinstance(key, int): + return self._tests.get(key, MonitorTest()) + elif isinstance(key, str) or isinstance(key, unicode): + return self.__dict__.get(key, MonitorTest()) + else: + logger.warning("Monitor test results can only be retrieved by TID value or property name") + class MonitorTest(): diff --git a/obd/codes.py b/obd/codes.py index 69922c89..71e5129a 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -2209,16 +2209,16 @@ TEST_IDS = { # : # 0x0 is reserved - 0x01 : ("rtl_threshold_voltage", "Rich to lean sensor threshold voltage"), - 0x02 : ("ltr_threshold_voltage", "Lean to rich sensor threshold voltage"), - 0x03 : ("low_voltage_switch_time", "Low sensor voltage for switch time calculation"), - 0x04 : ("high_voltage_switch_time", "High sensor voltage for switch time calculation"), - 0x05 : ("rtl_switch_time", "Rich to lean sensor switch time"), - 0x06 : ("ltr_switch_time", "Lean to rich sensor switch time"), - 0x07 : ("min_voltage", "Minimum sensor voltage for test cycle"), - 0x08 : ("max_voltage", "Maximum sensor voltage for test cycle"), - 0x09 : ("transition_time", "Time between sensor transitions"), - 0x0A : ("sensor_period", "Sensor period"), - 0x0B : ("misfire_average", "Average misfire counts for last ten driving cycles"), - 0x0C : ("misfire_count", "Misfire counts for last/current driving cycles"), + 0x01 : ("RTL_THRESHOLD_VOLTAGE", "Rich to lean sensor threshold voltage"), + 0x02 : ("LTR_THRESHOLD_VOLTAGE", "Lean to rich sensor threshold voltage"), + 0x03 : ("LOW_VOLTAGE_SWITCH_TIME", "Low sensor voltage for switch time calculation"), + 0x04 : ("HIGH_VOLTAGE_SWITCH_TIME", "High sensor voltage for switch time calculation"), + 0x05 : ("RTL_SWITCH_TIME", "Rich to lean sensor switch time"), + 0x06 : ("LTR_SWITCH_TIME", "Lean to rich sensor switch time"), + 0x07 : ("MIN_VOLTAGE", "Minimum sensor voltage for test cycle"), + 0x08 : ("MAX_VOLTAGE", "Maximum sensor voltage for test cycle"), + 0x09 : ("TRANSITION_TIME", "Time between sensor transitions"), + 0x0A : ("SENSOR_PERIOD", "Sensor period"), + 0x0B : ("MISFIRE_AVERAGE", "Average misfire counts for last ten driving cycles"), + 0x0C : ("MISFIRE_COUNT", "Misfire counts for last/current driving cycles"), } diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 1735391d..c339f466 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -188,7 +188,7 @@ def test_monitor(): assert len(v) == 1 # 1 test result # make sure we can look things up by name and TID - assert v[0x01] == v.rtl_threshold_voltage + assert v[0x01] == v.RTL_THRESHOLD_VOLTAGE == v["RTL_THRESHOLD_VOLTAGE"] # make sure we got information assert not v[0x01].is_null() @@ -203,8 +203,8 @@ def test_monitor(): assert len(v) == 3 # 3 test results # make sure we can look things up by name and TID - assert v[0x01] == v.rtl_threshold_voltage - assert v[0x05] == v.rtl_switch_time + assert v[0x01] == v.RTL_THRESHOLD_VOLTAGE == v["RTL_THRESHOLD_VOLTAGE"] + assert v[0x05] == v.RTL_SWITCH_TIME == v["RTL_SWITCH_TIME"] # make sure we got information assert not v[0x01].is_null() @@ -229,7 +229,7 @@ def test_monitor(): assert len(v) == 1 # 1 test result # make sure we can look things up by name and TID - assert v[0x01] == v.rtl_threshold_voltage + assert v[0x01] == v.RTL_THRESHOLD_VOLTAGE == v["RTL_THRESHOLD_VOLTAGE"] # make sure we got information assert not v[0x01].is_null() From ba3c3d4eb4f82d717940a5acae2ca0f67582733e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 5 Jul 2016 18:51:01 -0400 Subject: [PATCH 431/569] doc'd mode 06 responses --- docs/Responses.md | 62 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/docs/Responses.md b/docs/Responses.md index c7fa52d1..8277981b 100644 --- a/docs/Responses.md +++ b/docs/Responses.md @@ -115,10 +115,68 @@ response.value[1][2] == True # Bank 1, Sensor 2 is present # Monitors (Mode 06 Responses) -All mode 06 commands return `Monitor` objects holding various test results for the requested sensor. A single monitor response can hold multiple tests. +All mode 06 commands return `Monitor` objects holding various test results for the requested sensor. A single monitor response can hold multiple tests, in the form of `MonitorTest` objects. The OBD standard defines some tests, but vehicles can always implement custom tests beyond the standard. Here are the standard Test IDs (TIDs) that python-OBD will recognize: + +| TID | Name | Description | +|-----|--------------------------|----------------------------------------------------| +| 01 | RTL_THRESHOLD_VOLTAGE | Rich to lean sensor threshold voltage | +| 02 | LTR_THRESHOLD_VOLTAGE | Lean to rich sensor threshold voltage | +| 03 | LOW_VOLTAGE_SWITCH_TIME | Low sensor voltage for switch time calculation | +| 04 | HIGH_VOLTAGE_SWITCH_TIME | High sensor voltage for switch time calculation | +| 05 | RTL_SWITCH_TIME | Rich to lean sensor switch time | +| 06 | LTR_SWITCH_TIME | Lean to rich sensor switch time | +| 07 | MIN_VOLTAGE | Minimum sensor voltage for test cycle | +| 08 | MAX_VOLTAGE | Maximum sensor voltage for test cycle | +| 09 | TRANSITION_TIME | Time between sensor transitions | +| 0A | SENSOR_PERIOD | Sensor period | +| 0B | MISFIRE_AVERAGE | Average misfire counts for last ten driving cycles | +| 0C | MISFIRE_COUNT | Misfire counts for last/current driving cycles | + +Test results can be accessed by property name or TID (same as the `obd.commands` tables). All of the standard tests above will be present, though some may be null. Use the `MonitorTest.is_null()` function to determine if a test is null. ```python -# TODO +response.value.MISFIRE_COUNT + +# OR + +response.value["MISFIRE_COUNT"] + +# OR + +response.value[0x0C] # TID for MISFIRE_COUNT +``` + +All `MonitorTest` objects have the following properties: (for null tests, these are set to `None`) + +```python +result = response.value.MISFIRE_COUNT + +result.tid # integer Test ID for this test +result.name # test name +result.desc # test description +result.value # value of the test (will be a Pint value, or in rare cases, a boolean) +result.min # maximum acceptable value +result.max # minimum acceptable value +result.passed # boolean marking the test as passing +``` + +Here is an example of looking up live misfire counts for the engine's second cylinder: + +```python +import obd + +connection = obd.OBD() + +response = connection.query(obd.commands.MONITOR_MISFIRE_CYLINDER_2) + +# in the test results, lookup the result for MISFIRE_COUNT +result = response.value.MISFIRE_COUNT + +# check that we got data for this test +if not result.is_null(): + print(result.value) # will be a Pint value +else: + print("Misfire count wasn't reported") ``` --- From 30d79f2e404cb5b7164cbdff32730e0d5be92c38 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 5 Jul 2016 18:53:31 -0400 Subject: [PATCH 432/569] lists are only used on certain DTC commands --- docs/Responses.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Responses.md b/docs/Responses.md index 8277981b..3cdacdcb 100644 --- a/docs/Responses.md +++ b/docs/Responses.md @@ -71,7 +71,7 @@ import obd # Diagnostic Trouble Codes (DTCs) -Each DTC is represented by a tuple containing the DTC code, and a description (if python-OBD has one). When multiple DTCs are returned, they are stored in a list. +Each DTC is represented by a tuple containing the DTC code, and a description (if python-OBD has one). For commands that return multiple DTCs, a list is used. ```python # obd.commands.GET_DTC From 43af43ec7489b19dc708a2f0bc719ec461a254dc Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 5 Jul 2016 22:29:20 -0400 Subject: [PATCH 433/569] started writing tests for/tweaking status decoder --- docs/Responses.md | 10 ++++++++++ obd/decoders.py | 4 ++-- tests/test_decoders.py | 5 +++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/Responses.md b/docs/Responses.md index 3cdacdcb..396fb267 100644 --- a/docs/Responses.md +++ b/docs/Responses.md @@ -69,6 +69,16 @@ import obd --- +# Status + + + +```python + +``` + +--- + # Diagnostic Trouble Codes (DTCs) Each DTC is represented by a tuple containing the DTC code, and a description (if python-OBD has one). For commands that return multiple DTCs, a list is used. diff --git a/obd/decoders.py b/obd/decoders.py index a0501cd6..dc9667dc 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -247,8 +247,8 @@ def status(messages): bits = bytes_to_bits(d) output = Status() - output.MIL = bitToBool(bits[0]) - output.DTC_count = unbin(bits[1:8]) + output.MIL = bool(d[0] & 0b10000000) + output.DTC_count = d[0] & 0b01111111 output.ignition_type = IGNITION_TYPE[unbin(bits[12])] output.tests.append(Test("Misfire", \ diff --git a/tests/test_decoders.py b/tests/test_decoders.py index c339f466..41aa4063 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -146,6 +146,11 @@ def test_elm_voltage(): assert d.elm_voltage([ Message([ Frame("12") ]) ]) == 12 * Unit.volt assert d.elm_voltage([ Message([ Frame("12ABCD") ]) ]) == None +def test_status(): + status = d.status(m("83E0FF00")) + assert status.MIL + assert status.DTC_count == 3 + def test_single_dtc(): assert d.single_dtc(m("0104")) == ("P0104", "Mass or Volume Air Flow Circuit Intermittent") assert d.single_dtc(m("4123")) == ("C0123", "") From b3c801fa5aa9b76afa39ff40fbc0b251fc419f53 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 14:01:30 -0400 Subject: [PATCH 434/569] wrote quick and dirty bitarray class --- obd/utils.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/obd/utils.py b/obd/utils.py index 942c2fdc..c75fac5b 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -50,6 +50,37 @@ class OBDStatus: +class bitarray: + """ + Class for representing bitarrays (inefficiently) + + There's a nice C-optimized lib for this: https://github.com/ilanschnell/bitarray + but python-OBD doesn't use it enough to be worth adding the dependency. + But, if this class starts getting used too much, we should switch to that lib. + """ + + def __init__(self, _bytearray): + bits = "" + + for b in _bytearray[::-1]: # put the bytes in bit-number order + v = bin(b)[2:] + bits += ("0" * (8 - len(v))) + v # pad it with zeros + self.bits = bits[::-1] # reverse, to maintain zero indexing + + def __getitem__(self, key): + if isinstance(key, int): + if key >= 0 and key < len(self.bits): + return self.bits[key] == "1" + else: + return False + elif isinstance(key, slice): + bits = self.bits[key][::-1] # reverse back into correct bit-order + if bits: + return int(bits, 2) + else: + return 0 + + def num_bits_set(n): return bin(n).count("1") From 976775a2cd69294991010ac145a2f9b4c637e182 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 14:05:14 -0400 Subject: [PATCH 435/569] added __str__ for easy debugging --- obd/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/obd/utils.py b/obd/utils.py index c75fac5b..22d7d08b 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -61,7 +61,6 @@ class bitarray: def __init__(self, _bytearray): bits = "" - for b in _bytearray[::-1]: # put the bytes in bit-number order v = bin(b)[2:] bits += ("0" * (8 - len(v))) + v # pad it with zeros @@ -80,6 +79,9 @@ def __getitem__(self, key): else: return 0 + def __str__(self): + return self.bits[::-1] + def num_bits_set(n): return bin(n).count("1") From 37cb7638a2cbee86ad34a48d30a46498e1739c08 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 14:31:04 -0400 Subject: [PATCH 436/569] using bitarray in PID decoder --- obd/decoders.py | 3 +-- obd/obd.py | 8 +++----- obd/utils.py | 23 +++++++++++++++++------ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index dc9667dc..6bd0df31 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -64,8 +64,7 @@ def noop(messages): # hex in, bitstring out def pid(messages): d = messages[0].data - v = bytes_to_bits(d) - return v + return bitarray(d) # returns the raw strings from the ELM def raw_string(messages): diff --git a/obd/obd.py b/obd/obd.py index 97504ebc..654dcf5b 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -114,11 +114,9 @@ def __load_commands(self): if response.is_null(): continue - supported = response.value # string of binary 01010101010101 - - # loop through PIDs binary - for i in range(len(supported)): - if supported[i] == "1": + # loop through PIDs bitarray + for i, bit in enumerate(response.value): + if bit: mode = get.mode pid = get.pid + i + 1 diff --git a/obd/utils.py b/obd/utils.py index 22d7d08b..5d80edd0 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -60,11 +60,10 @@ class bitarray: """ def __init__(self, _bytearray): - bits = "" - for b in _bytearray[::-1]: # put the bytes in bit-number order + self.bits = "" + for b in _bytearray: # put the bytes in bit-number order v = bin(b)[2:] - bits += ("0" * (8 - len(v))) + v # pad it with zeros - self.bits = bits[::-1] # reverse, to maintain zero indexing + self.bits += ("0" * (8 - len(v))) + v # pad it with zeros def __getitem__(self, key): if isinstance(key, int): @@ -73,14 +72,26 @@ def __getitem__(self, key): else: return False elif isinstance(key, slice): - bits = self.bits[key][::-1] # reverse back into correct bit-order + bits = self.bits[key] # reverse back into correct bit-order if bits: return int(bits, 2) else: return 0 + def num_set(self): + return self.bits.count("1") + + def num_cleared(self): + return self.bits.count("0") + + def __len__(self): + return len(self.bits) + def __str__(self): - return self.bits[::-1] + return self.bits + + def __iter__(self): + return [ b == "1" for b in self.bits ].__iter__() def num_bits_set(n): From 6331a7f53e5b51a5897e2b36b696ae9a1c6e8f76 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 14:45:40 -0400 Subject: [PATCH 437/569] simplified status decoder --- obd/decoders.py | 53 +++++++++++++++++-------------------------------- 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 6bd0df31..83178a06 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -243,46 +243,29 @@ def elm_voltage(messages): def status(messages): d = messages[0].data - bits = bytes_to_bits(d) + bits = bitarray(d) output = Status() - output.MIL = bool(d[0] & 0b10000000) - output.DTC_count = d[0] & 0b01111111 - output.ignition_type = IGNITION_TYPE[unbin(bits[12])] - - output.tests.append(Test("Misfire", \ - bitToBool(bits[15]), \ - bitToBool(bits[11]))) - - output.tests.append(Test("Fuel System", \ - bitToBool(bits[14]), \ - bitToBool(bits[10]))) - - output.tests.append(Test("Components", \ - bitToBool(bits[13]), \ - bitToBool(bits[9]))) + output.MIL = bits[7] + output.DTC_count = bits[0:7] + output.ignition_type = IGNITION_TYPE[int(bits[12])] + output.tests.append(Test("Misfire", bits[15], bits[11])) + output.tests.append(Test("Fuel System", bits[14], bits[10])) + output.tests.append(Test("Components", bits[13], bits[9])) # different tests for different ignition types - if(output.ignition_type == IGNITION_TYPE[0]): # spark - for i in range(8): - if SPARK_TESTS[i] is not None: - - t = Test(SPARK_TESTS[i], \ - bitToBool(bits[(2 * 8) + i]), \ - bitToBool(bits[(3 * 8) + i])) - - output.tests.append(t) - - elif(output.ignition_type == IGNITION_TYPE[1]): # compression - for i in range(8): - if COMPRESSION_TESTS[i] is not None: - - t = Test(COMPRESSION_TESTS[i], \ - bitToBool(bits[(2 * 8) + i]), \ - bitToBool(bits[(3 * 8) + i])) - - output.tests.append(t) + if bits[12]: # ignition type: compression + for i, name in enumerate(COMPRESSION_TESTS): + t = Test(name, bits[(2 * 8) + i], + bits[(3 * 8) + i]) + output.tests.append(t) + + else: # ignition type: spark + for i, name in enumerate(SPARK_TESTS): + t = Test(name, bits[(2 * 8) + i], + bits[(3 * 8) + i]) + output.tests.append(t) return output From 0cc539fffc0409c1fc57c0bd2a3493ca139c1ec3 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 17:14:48 -0400 Subject: [PATCH 438/569] reimplemented status, wrote test --- obd/OBDResponse.py | 21 ++++++++----- obd/codes.py | 42 +++++++++++++++----------- obd/decoders.py | 46 +++++++++++++++++----------- obd/utils.py | 4 +-- tests/test_decoders.py | 68 ++++++++++++++++++++++++++++++++++++++---- 5 files changed, 131 insertions(+), 50 deletions(-) diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py index f236699b..e3c7d550 100644 --- a/obd/OBDResponse.py +++ b/obd/OBDResponse.py @@ -77,18 +77,25 @@ def __init__(self): self.MIL = False self.DTC_count = 0 self.ignition_type = "" - self.tests = [] + + # make sure each test is available by name + # until real data comes it. This also prevents things from + # breaking when the user looks up a standard test that's null. + null_test = StatusTest() + for name in BASE_TESTS + SPARK_TESTS + COMPRESSION_TESTS: + if name: # filter out None/reserved tests + self.__dict__[name] = null_test -class Test(): - def __init__(self, name, available, incomplete): - self.name = name - self.available = available - self.incomplete = incomplete +class StatusTest(): + def __init__(self, name="", available=False, complete=False): + self.name = name + self.available = available + self.complete = complete def __str__(self): a = "Available" if self.available else "Unavailable" - c = "Incomplete" if self.incomplete else "Complete" + c = "Complete" if self.complete else "Incomplete" return "Test %s: %s, %s" % (self.name, a, c) diff --git a/obd/codes.py b/obd/codes.py index 71e5129a..97202e92 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -2101,30 +2101,36 @@ } IGNITION_TYPE = [ - "Spark", - "Compression", + "spark", + "compression", +] + +BASE_TESTS = [ + "MISFIRE_MONITORING", + "FUEL_SYSTEM_MONITORING", + "COMPONENT_MONITORING", ] SPARK_TESTS = [ - "EGR System", - "Oxygen Sensor Heater", - "Oxygen Sensor", - "A/C Refrigerant", - "Secondary Air System", - "Evaporative System", - "Heated Catalyst", - "Catalyst", + "CATALYST_MONITORING", + "HEATED_CATALYST_MONITORING", + "EVAPORATIVE_SYSTEM_MONITORING", + "SECONDARY_AIR_SYSTEM_MONITORING", + None, + "OXYGEN_SENSOR_MONITORING", + "OXYGEN_SENSOR_HEATER_MONITORING", + "EGR_VVT_SYSTEM_MONITORING" ] COMPRESSION_TESTS = [ - "EGR and/or VVT System", - "PM filter monitoring", - "Exhaust Gas Sensor", - "None", - "Boost Pressure", - "None", - "NOx/SCR Monitor", - "NMHC Catalyst", + "NMHC_CATALYST_MONITORING", + "NOX_SCR_AFTERTREATMENT_MONITORING", + None, + "BOOST_PRESSURE_MONITORING", + None, + "EXHAUST_GAS_SENSOR_MONITORING", + "PM_FILTER_MONITORING", + "EGR_VVT_SYSTEM_MONITORING", ] FUEL_STATUS = [ diff --git a/obd/decoders.py b/obd/decoders.py index 83178a06..5edb6344 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -33,7 +33,7 @@ import functools from .utils import * from .codes import * -from .OBDResponse import Status, Test, Monitor, MonitorTest +from .OBDResponse import Status, StatusTest, Monitor, MonitorTest from .UnitsAndScaling import Unit, UAS_IDS import logging @@ -245,27 +245,39 @@ def status(messages): d = messages[0].data bits = bitarray(d) + # ┌Components not ready + # |┌Fuel not ready + # ||┌Misfire not ready + # |||┌Spark vs. Compression + # ||||┌Components supported + # |||||┌Fuel supported + # ┌MIL ||||||┌Misfire supported + # | ||||||| + # 10000011 00000111 11111111 00000000 + # [# DTC] X [supprt] [~ready] + output = Status() - output.MIL = bits[7] - output.DTC_count = bits[0:7] + output.MIL = bits[0] + output.DTC_count = bits[1:8] output.ignition_type = IGNITION_TYPE[int(bits[12])] - output.tests.append(Test("Misfire", bits[15], bits[11])) - output.tests.append(Test("Fuel System", bits[14], bits[10])) - output.tests.append(Test("Components", bits[13], bits[9])) + # load the 3 base tests that are always present + for i, name in enumerate(BASE_TESTS[::-1]): + t = StatusTest(name, bits[13 + i], not bits[9 + i]) + output.__dict__[name] = t # different tests for different ignition types - if bits[12]: # ignition type: compression - for i, name in enumerate(COMPRESSION_TESTS): - t = Test(name, bits[(2 * 8) + i], - bits[(3 * 8) + i]) - output.tests.append(t) - - else: # ignition type: spark - for i, name in enumerate(SPARK_TESTS): - t = Test(name, bits[(2 * 8) + i], - bits[(3 * 8) + i]) - output.tests.append(t) + if bits[12]: # compression + for i, name in enumerate(COMPRESSION_TESTS[::-1]): # reverse to correct for bit vs. indexing order + t = StatusTest(name, bits[(2 * 8) + i], + not bits[(3 * 8) + i]) + output.__dict__[name] = t + + else: # spark + for i, name in enumerate(SPARK_TESTS[::-1]): # reverse to correct for bit vs. indexing order + t = StatusTest(name, bits[(2 * 8) + i], + not bits[(3 * 8) + i]) + output.__dict__[name] = t return output diff --git a/obd/utils.py b/obd/utils.py index 5d80edd0..5e576f88 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -61,7 +61,7 @@ class bitarray: def __init__(self, _bytearray): self.bits = "" - for b in _bytearray: # put the bytes in bit-number order + for b in _bytearray: v = bin(b)[2:] self.bits += ("0" * (8 - len(v))) + v # pad it with zeros @@ -72,7 +72,7 @@ def __getitem__(self, key): else: return False elif isinstance(key, slice): - bits = self.bits[key] # reverse back into correct bit-order + bits = self.bits[key] if bits: return int(bits, 2) else: diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 41aa4063..da955a06 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -3,7 +3,7 @@ from obd.UnitsAndScaling import Unit from obd.protocols.protocol import Frame, Message -from obd.codes import TEST_IDS +from obd.codes import BASE_TESTS, COMPRESSION_TESTS, SPARK_TESTS, TEST_IDS import obd.decoders as d @@ -41,9 +41,9 @@ def test_raw_string(): assert d.raw_string([ Message([ Frame("A") ]), Message([ Frame("B") ]) ]) == "A\nB" def test_pid(): - assert d.pid(m("00000000")) == "00000000000000000000000000000000" - assert d.pid(m("F00AA00F")) == "11110000000010101010000000001111" - assert d.pid(m("11")) == "00010001" + assert d.pid(m("00000000")).bits == "00000000000000000000000000000000" + assert d.pid(m("F00AA00F")).bits == "11110000000010101010000000001111" + assert d.pid(m("11")).bits == "00010001" def test_percent(): assert d.percent(m("00")) == 0.0 * Unit.percent @@ -147,13 +147,69 @@ def test_elm_voltage(): assert d.elm_voltage([ Message([ Frame("12ABCD") ]) ]) == None def test_status(): - status = d.status(m("83E0FF00")) + status = d.status(m("8307FF00")) assert status.MIL assert status.DTC_count == 3 + assert status.ignition_type == "spark" + + for name in BASE_TESTS: + assert status.__dict__[name].available + assert status.__dict__[name].complete + + # check that NONE of the compression tests are available + for name in COMPRESSION_TESTS: + if name and name not in SPARK_TESTS: # there's one test name in common between spark/compression + assert not status.__dict__[name].available + assert not status.__dict__[name].complete + + # check that ALL of the spark tests are availablex + for name in SPARK_TESTS: + if name: + assert status.__dict__[name].available + assert status.__dict__[name].complete + + # a different test + status = d.status(m("00790303")) + assert not status.MIL + assert status.DTC_count == 0 + assert status.ignition_type == "compression" + + # availability + assert status.MISFIRE_MONITORING.available + assert not status.FUEL_SYSTEM_MONITORING.available + assert not status.COMPONENT_MONITORING.available + + # completion + assert not status.MISFIRE_MONITORING.complete + assert not status.FUEL_SYSTEM_MONITORING.complete + assert not status.COMPONENT_MONITORING.complete + + # check that NONE of the spark tests are availablex + for name in SPARK_TESTS: + if name and name not in COMPRESSION_TESTS: + assert not status.__dict__[name].available + assert not status.__dict__[name].complete + + # availability + assert status.NMHC_CATALYST_MONITORING.available + assert status.NOX_SCR_AFTERTREATMENT_MONITORING.available + assert not status.BOOST_PRESSURE_MONITORING.available + assert not status.EXHAUST_GAS_SENSOR_MONITORING.available + assert not status.PM_FILTER_MONITORING.available + assert not status.EGR_VVT_SYSTEM_MONITORING.available + + # completion + assert not status.NMHC_CATALYST_MONITORING.complete + assert not status.NOX_SCR_AFTERTREATMENT_MONITORING.complete + assert status.BOOST_PRESSURE_MONITORING.complete + assert status.EXHAUST_GAS_SENSOR_MONITORING.complete + assert status.PM_FILTER_MONITORING.complete + assert status.EGR_VVT_SYSTEM_MONITORING.complete + def test_single_dtc(): assert d.single_dtc(m("0104")) == ("P0104", "Mass or Volume Air Flow Circuit Intermittent") - assert d.single_dtc(m("4123")) == ("C0123", "") + assert d.single_dtc(m("4123")) == ("C0123", "") # reverse back into correct bit-order assert d.single_dtc(m("01")) == None assert d.single_dtc(m("010400")) == None From 5d1c5024e94d15db2821e57678e4563e29898813 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 17:32:20 -0400 Subject: [PATCH 439/569] wrote docs for status command --- docs/Responses.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/Responses.md b/docs/Responses.md index 396fb267..63e93fdf 100644 --- a/docs/Responses.md +++ b/docs/Responses.md @@ -71,12 +71,42 @@ import obd # Status - +The status command returns information about the Malfunction Indicator Light (check-engine light), the number of trouble codes being thrown, and the type of engine. ```python +response.value.MIL # boolean for whether the check-engine is lit +response.value.DTC_count # number (int) of DTCs being thrown +responce.value.ignition_type # "spark" or "compression" +``` + +The status command also provides information regarding the availability and status of various system tests. These are exposed as `StatusTest` objects, loaded into named properties. Each test object has boolean flags for its availability and completion. +```python +response.value.MISFIRE_MONITORING.available # boolean for test availability +response.value.MISFIRE_MONITORING.complete # boolean for test completion ``` +Here are all of the tests names that python-OBD reports: + +| Tests | +|-----------------------------------| +| MISFIRE_MONITORING | +| FUEL_SYSTEM_MONITORING | +| COMPONENT_MONITORING | +| CATALYST_MONITORING | +| HEATED_CATALYST_MONITORING | +| EVAPORATIVE_SYSTEM_MONITORING | +| SECONDARY_AIR_SYSTEM_MONITORING | +| OXYGEN_SENSOR_MONITORING | +| OXYGEN_SENSOR_HEATER_MONITORING | +| EGR_VVT_SYSTEM_MONITORING | +| NMHC_CATALYST_MONITORING | +| NOX_SCR_AFTERTREATMENT_MONITORING | +| BOOST_PRESSURE_MONITORING | +| EXHAUST_GAS_SENSOR_MONITORING | +| PM_FILTER_MONITORING | + + --- # Diagnostic Trouble Codes (DTCs) From de288cbf03b268e74871d6a3b3f80e6b6f0b0e84 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 17:33:29 -0400 Subject: [PATCH 440/569] link from commands table to status response description --- docs/Commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Commands.md b/docs/Commands.md index bd39dd4f..952829ba 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -75,7 +75,7 @@ obd.commands.has_pid(1, 12) # True |PID | Name | Description | Response Value | |----|---------------------------|-----------------------------------------|-----------------------| | 00 | PIDS_A | Supported PIDs [01-20] | bitstring | -| 01 | STATUS | Status since DTCs cleared | | +| 01 | STATUS | Status since DTCs cleared | [special](Responses.md#status) | | 02 | FREEZE_DTC | DTC that triggered the freeze frame | [special](Responses.md#diagnostic-trouble-codes-dtcs) | | 03 | FUEL_STATUS | Fuel System Status | string | | 04 | ENGINE_LOAD | Calculated Engine Load | Unit.percent | From 44c1896bd7874d9b4177d17d20f8e9fc4bb390c5 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 17:52:13 -0400 Subject: [PATCH 441/569] removed old bit utils --- obd/decoders.py | 18 +++++++++--------- obd/utils.py | 21 +++++++++------------ 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 5edb6344..156a9001 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -199,11 +199,11 @@ def fuel_rate(messages): # special bit encoding for PID 13 def o2_sensors(messages): d = messages[0].data - bitstring = bytes_to_bits(d) + bits = bitarray(d) return ( (), # bank 0 is invalid - tuple([ b == "1" for b in bitstring[:4] ]), # bank 1 - tuple([ b == "1" for b in bitstring[4:] ]), # bank 2 + tuple(bits[:4]), # bank 1 + tuple(bits[4:]), # bank 2 ) def aux_input_status(messages): @@ -213,13 +213,13 @@ def aux_input_status(messages): # special bit encoding for PID 1D def o2_sensors_alt(messages): d = messages[0].data - bitstring = bytes_to_bits(d) + bits = bitarray(d) return ( (), # bank 0 is invalid - tuple([ b == "1" for b in bitstring[:2] ]), # bank 1 - tuple([ b == "1" for b in bitstring[2:4] ]), # bank 2 - tuple([ b == "1" for b in bitstring[4:6] ]), # bank 3 - tuple([ b == "1" for b in bitstring[6:] ]), # bank 4 + tuple(bits[:2]), # bank 1 + tuple(bits[2:4]), # bank 2 + tuple(bits[4:6]), # bank 3 + tuple(bits[6:]), # bank 4 ) def elm_voltage(messages): @@ -258,7 +258,7 @@ def status(messages): output = Status() output.MIL = bits[0] - output.DTC_count = bits[1:8] + output.DTC_count = bits.value(1, 8) output.ignition_type = IGNITION_TYPE[int(bits[12])] # load the 3 base tests that are always present diff --git a/obd/utils.py b/obd/utils.py index 5e576f88..73ba151b 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -74,9 +74,9 @@ def __getitem__(self, key): elif isinstance(key, slice): bits = self.bits[key] if bits: - return int(bits, 2) + return [ b == "1" for b in bits ] else: - return 0 + return [] def num_set(self): return self.bits.count("1") @@ -84,6 +84,13 @@ def num_set(self): def num_cleared(self): return self.bits.count("0") + def value(self, start, stop): + bits = self.bits[start:stop] + if bits: + return int(bits, 2) + else: + return 0 + def __len__(self): return len(self.bits) @@ -97,9 +104,6 @@ def __iter__(self): def num_bits_set(n): return bin(n).count("1") -def unbin(_bin): - return int(_bin, 2) - def bytes_to_int(bs): """ converts a big-endian byte array into a single integer """ v = 0 @@ -109,13 +113,6 @@ def bytes_to_int(bs): p += 8 return v -def bytes_to_bits(bs): - bits = "" - for b in bs: - v = bin(b)[2:] - bits += ("0" * (8 - len(v))) + v # pad it with zeros - return bits - def bytes_to_hex(bs): h = "" for b in bs: From 69fc0424ddf67ebeb2333ddb4ef3b501f81f829b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 18:08:12 -0400 Subject: [PATCH 442/569] rewrote fuel status, using bitarrays, read second system --- obd/decoders.py | 28 ++++++++++++++-------------- tests/test_decoders.py | 8 ++++++-- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 156a9001..64e7d87c 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -285,25 +285,25 @@ def status(messages): def fuel_status(messages): d = messages[0].data - v = d[0] # todo, support second fuel system - - if v <= 0: - logger.debug("Invalid fuel status response (v <= 0)") - return None + bits = bitarray(d) - i = math.log(v, 2) # only a single bit should be on + status_1 = "" + status_2 = "" - if i % 1 != 0: - logger.debug("Invalid fuel status response (multiple bits set)") - return None + if bits[0:8].count(True) == 1: + status_1 = FUEL_STATUS[ 7 - bits[0:8].index(True) ] + else: + logger.debug("Invalid response for fuel status (multiple/no bits set)") - i = int(i) + if bits[8:16].count(True) == 1: + status_2 = FUEL_STATUS[ 7 - bits[8:16].index(True) ] + else: + logger.debug("Invalid response for fuel status (multiple/no bits set)") - if i >= len(FUEL_STATUS): - logger.debug("Invalid fuel status response (no table entry)") + if not status_1 and not status_2: return None - - return FUEL_STATUS[i] + else: + return (status_1, status_2) def air_status(messages): diff --git a/tests/test_decoders.py b/tests/test_decoders.py index da955a06..6d33f1d0 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -115,9 +115,13 @@ def test_fuel_rate(): assert d.fuel_rate(m("FFFF")) == 3276.75 * Unit.liters_per_hour def test_fuel_status(): - assert d.fuel_status(m("0100")) == "Open loop due to insufficient engine temperature" - assert d.fuel_status(m("0800")) == "Open loop due to system failure" + assert d.fuel_status(m("0100")) == ("Open loop due to insufficient engine temperature", "") + assert d.fuel_status(m("0800")) == ("Open loop due to system failure", "") + assert d.fuel_status(m("0808")) == ("Open loop due to system failure", + "Open loop due to system failure") + assert d.fuel_status(m("0000")) == None assert d.fuel_status(m("0300")) == None + assert d.fuel_status(m("0303")) == None def test_air_status(): assert d.air_status(m("01")) == "Upstream" From 7d039a36913d4c2ad479f1900eb8a2d7e96bf97d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 18:26:25 -0400 Subject: [PATCH 443/569] added fuel and air status links --- docs/Commands.md | 22 +++++++++++----------- docs/Responses.md | 28 ++++++++++++++++++++++++++++ tests/test_decoders.py | 1 + 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/docs/Commands.md b/docs/Commands.md index 952829ba..fbd54eae 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -74,10 +74,10 @@ obd.commands.has_pid(1, 12) # True |PID | Name | Description | Response Value | |----|---------------------------|-----------------------------------------|-----------------------| -| 00 | PIDS_A | Supported PIDs [01-20] | bitstring | +| 00 | PIDS_A | Supported PIDs [01-20] | bitarray | | 01 | STATUS | Status since DTCs cleared | [special](Responses.md#status) | | 02 | FREEZE_DTC | DTC that triggered the freeze frame | [special](Responses.md#diagnostic-trouble-codes-dtcs) | -| 03 | FUEL_STATUS | Fuel System Status | string | +| 03 | FUEL_STATUS | Fuel System Status | [(string, string)](Responses.md#fuel-status) | | 04 | ENGINE_LOAD | Calculated Engine Load | Unit.percent | | 05 | COOLANT_TEMP | Engine Coolant Temperature | Unit.celsius | | 06 | SHORT_FUEL_TRIM_1 | Short Term Fuel Trim - Bank 1 | Unit.percent | @@ -92,7 +92,7 @@ obd.commands.has_pid(1, 12) # True | 0F | INTAKE_TEMP | Intake Air Temp | Unit.celsius | | 10 | MAF | Air Flow Rate (MAF) | Unit.grams_per_second | | 11 | THROTTLE_POS | Throttle Position | Unit.percent | -| 12 | AIR_STATUS | Secondary Air Status | string | +| 12 | AIR_STATUS | Secondary Air Status | [string](Responses.md#air-status) | | 13 | O2_SENSORS | O2 Sensors Present | [special](Responses.md#oxygen-sensors-present) | | 14 | O2_B1S1 | O2: Bank 1 - Sensor 1 Voltage | Unit.volt | | 15 | O2_B1S2 | O2: Bank 1 - Sensor 2 Voltage | Unit.volt | @@ -106,7 +106,7 @@ obd.commands.has_pid(1, 12) # True | 1D | O2_SENSORS_ALT | O2 Sensors Present (alternate) | [special](Responses.md#oxygen-sensors-present) | | 1E | AUX_INPUT_STATUS | Auxiliary input status (power take off) | boolean | | 1F | RUN_TIME | Engine Run Time | Unit.second | -| 20 | PIDS_B | Supported PIDs [21-40] | bitstring | +| 20 | PIDS_B | Supported PIDs [21-40] | bitarray | | 21 | DISTANCE_W_MIL | Distance Traveled with MIL on | Unit.kilometer | | 22 | FUEL_RAIL_PRESSURE_VAC | Fuel Rail Pressure (relative to vacuum) | Unit.kilopascal | | 23 | FUEL_RAIL_PRESSURE_DIRECT | Fuel Rail Pressure (direct inject) | Unit.kilopascal | @@ -138,7 +138,7 @@ obd.commands.has_pid(1, 12) # True | 3D | CATALYST_TEMP_B2S1 | Catalyst Temperature: Bank 2 - Sensor 1 | Unit.celsius | | 3E | CATALYST_TEMP_B1S2 | Catalyst Temperature: Bank 1 - Sensor 2 | Unit.celsius | | 3F | CATALYST_TEMP_B2S2 | Catalyst Temperature: Bank 2 - Sensor 2 | Unit.celsius | -| 40 | PIDS_C | Supported PIDs [41-60] | bitstring | +| 40 | PIDS_C | Supported PIDs [41-60] | bitarray | | 41 | *unsupported* | *unsupported* | | | 42 | *unsupported* | *unsupported* | | | 43 | *unsupported* | *unsupported* | | @@ -212,7 +212,7 @@ Mode 06 commands are used to monitor various test results from the vehicle. All |PID | Name | Description | Response Value | |-------|-----------------------------|--------------------------------------------|-----------------------| -| 00 | MIDS_A | Supported MIDs [01-20] | bitstring | +| 00 | MIDS_A | Supported MIDs [01-20] | bitarray | | 01 | MONITOR_O2_B1S1 | O2 Sensor Monitor Bank 1 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) | | 02 | MONITOR_O2_B1S2 | O2 Sensor Monitor Bank 1 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) | | 03 | MONITOR_O2_B1S3 | O2 Sensor Monitor Bank 1 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) | @@ -230,7 +230,7 @@ Mode 06 commands are used to monitor various test results from the vehicle. All | 0F | MONITOR_O2_B4S3 | O2 Sensor Monitor Bank 4 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) | | 10 | MONITOR_O2_B4S4 | O2 Sensor Monitor Bank 4 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| 20 | MIDS_B | Supported MIDs [21-40] | bitstring | +| 20 | MIDS_B | Supported MIDs [21-40] | bitarray | | 21 | MONITOR_CATALYST_B1 | Catalyst Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | | 22 | MONITOR_CATALYST_B2 | Catalyst Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) | | 23 | MONITOR_CATALYST_B3 | Catalyst Monitor Bank 3 | [monitor](Responses.md#monitors-mode-06-responses) | @@ -250,7 +250,7 @@ Mode 06 commands are used to monitor various test results from the vehicle. All | 3C | MONITOR_EVAP_020 | EVAP Monitor (0.020\") | [monitor](Responses.md#monitors-mode-06-responses) | | 3D | MONITOR_PURGE_FLOW | Purge Flow Monitor | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| 40 | MIDS_C | Supported MIDs [41-60] | bitstring | +| 40 | MIDS_C | Supported MIDs [41-60] | bitarray | | 41 | MONITOR_O2_HEATER_B1S1 | O2 Sensor Heater Monitor Bank 1 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) | | 42 | MONITOR_O2_HEATER_B1S2 | O2 Sensor Heater Monitor Bank 1 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) | | 43 | MONITOR_O2_HEATER_B1S3 | O2 Sensor Heater Monitor Bank 1 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) | @@ -268,7 +268,7 @@ Mode 06 commands are used to monitor various test results from the vehicle. All | 4F | MONITOR_O2_HEATER_B4S3 | O2 Sensor Heater Monitor Bank 4 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) | | 50 | MONITOR_O2_HEATER_B4S4 | O2 Sensor Heater Monitor Bank 4 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| 60 | MIDS_D | Supported MIDs [61-80] | bitstring | +| 60 | MIDS_D | Supported MIDs [61-80] | bitarray | | 61 | MONITOR_HEATED_CATALYST_B1 | Heated Catalyst Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | | 62 | MONITOR_HEATED_CATALYST_B2 | Heated Catalyst Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) | | 63 | MONITOR_HEATED_CATALYST_B3 | Heated Catalyst Monitor Bank 3 | [monitor](Responses.md#monitors-mode-06-responses) | @@ -279,7 +279,7 @@ Mode 06 commands are used to monitor various test results from the vehicle. All | 73 | MONITOR_SECONDARY_AIR_3 | Secondary Air Monitor 3 | [monitor](Responses.md#monitors-mode-06-responses) | | 74 | MONITOR_SECONDARY_AIR_4 | Secondary Air Monitor 4 | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| 80 | MIDS_E | Supported MIDs [81-A0] | bitstring | +| 80 | MIDS_E | Supported MIDs [81-A0] | bitarray | | 81 | MONITOR_FUEL_SYSTEM_B1 | Fuel System Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | | 82 | MONITOR_FUEL_SYSTEM_B2 | Fuel System Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) | | 83 | MONITOR_FUEL_SYSTEM_B3 | Fuel System Monitor Bank 3 | [monitor](Responses.md#monitors-mode-06-responses) | @@ -293,7 +293,7 @@ Mode 06 commands are used to monitor various test results from the vehicle. All | 98 | MONITOR_NOX_CATALYST_B1 | NOx Catalyst Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | | 99 | MONITOR_NOX_CATALYST_B2 | NOx Catalyst Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| A0 | MIDS_F | Supported MIDs [A1-C0] | bitstring | +| A0 | MIDS_F | Supported MIDs [A1-C0] | bitarray | | A1 | MONITOR_MISFIRE_GENERAL | Misfire Monitor General Data | [monitor](Responses.md#monitors-mode-06-responses) | | A2 | MONITOR_MISFIRE_CYLINDER_1 | Misfire Cylinder 1 Data | [monitor](Responses.md#monitors-mode-06-responses) | | A3 | MONITOR_MISFIRE_CYLINDER_2 | Misfire Cylinder 2 Data | [monitor](Responses.md#monitors-mode-06-responses) | diff --git a/docs/Responses.md b/docs/Responses.md index 63e93fdf..0048d8fe 100644 --- a/docs/Responses.md +++ b/docs/Responses.md @@ -127,6 +127,34 @@ responce.value = ("P0104", "Mass or Volume Air Flow Circuit Intermittent") --- +# Fuel Status + +The fuel status is a tuple of two strings, telling the status of the first and second fuel systems. Most cars only have one system, so the second element will likely be an empty string. The possible fuel statuses are: + +| Fuel Status | +| ----------------------------------------------------------------------------------------------| +| `""` | +| `"Open loop due to insufficient engine temperature"` | +| `"Closed loop, using oxygen sensor feedback to determine fuel mix"` | +| `"Open loop due to engine load OR fuel cut due to deceleration"` | +| `"Open loop due to system failure"` | +| `"Closed loop, using at least one oxygen sensor but there is a fault in the feedback system"` | + +--- + +# Air Status + +The air status will be one of these strings: + +| Air Status | +| ---------------------------------------| +| `"Upstream"` | +| `"Downstream of catalytic converter"` | +| `"From the outside atmosphere or off"` | +| `"Pump commanded on for diagnostics"` | + +--- + # Oxygen Sensors Present Returns a 2D structure of tuples (representing bank and sensor number), that holds boolean values for sensor presence. diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 6d33f1d0..8c5e6977 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -119,6 +119,7 @@ def test_fuel_status(): assert d.fuel_status(m("0800")) == ("Open loop due to system failure", "") assert d.fuel_status(m("0808")) == ("Open loop due to system failure", "Open loop due to system failure") + assert d.fuel_status(m("0008")) == ("", "Open loop due to system failure") assert d.fuel_status(m("0000")) == None assert d.fuel_status(m("0300")) == None assert d.fuel_status(m("0303")) == None From 669827d688e3cb9b02756d7aea6988bd21337632 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 18:28:42 -0400 Subject: [PATCH 444/569] simplified air status decoder with bitarray --- obd/decoders.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 64e7d87c..f12ff41a 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -308,25 +308,15 @@ def fuel_status(messages): def air_status(messages): d = messages[0].data - v = d[0] - - if v <= 0: - logger.debug("Invalid air status response (v <= 0)") - return None - - i = math.log(v, 2) # only a single bit should be on - - if i % 1 != 0: - logger.debug("Invalid air status response (multiple bits set)") - return None - - i = int(i) + bits = bitarray(d) - if i >= len(AIR_STATUS): - logger.debug("Invalid air status response (no table entry)") - return None + status = None + if bits.num_set() == 1: + status = AIR_STATUS[ 7 - bits[0:8].index(True) ] + else: + logger.debug("Invalid response for fuel status (multiple/no bits set)") - return AIR_STATUS[i] + return status def obd_compliance(_hex): From e2676919a46b6ee3cdf9b5438f9b46a1d02b0216 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 18:34:42 -0400 Subject: [PATCH 445/569] removed lingering bit-handling functions --- obd/protocols/protocol.py | 4 ++-- obd/utils.py | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index d0c102ec..2dce7383 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -30,7 +30,7 @@ ######################################################################## from binascii import hexlify -from obd.utils import isHex, num_bits_set +from obd.utils import isHex, bitarray import logging @@ -267,7 +267,7 @@ def populate_ecu_map(self, messages): tx_id = None for message in messages: - bits = sum([num_bits_set(b) for b in message.data]) + bits = bitarray(message.data).num_set() if bits > best: best = bits diff --git a/obd/utils.py b/obd/utils.py index 73ba151b..b1061693 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -101,9 +101,6 @@ def __iter__(self): return [ b == "1" for b in self.bits ].__iter__() -def num_bits_set(n): - return bin(n).count("1") - def bytes_to_int(bs): """ converts a big-endian byte array into a single integer """ v = 0 @@ -120,9 +117,6 @@ def bytes_to_hex(bs): h += ("0" * (2 - len(bh))) + bh return h -def bitToBool(_bit): - return (_bit == '1') - def twos_comp(val, num_bits): """compute the 2's compliment of int value val""" if( (val&(1<<(num_bits-1))) != 0 ): From 1a54674cbbf01467a3043e63263fe70ab658b89d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 18:39:32 -0400 Subject: [PATCH 446/569] removed old reference to OBDResponse.unit --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 2b1583f6..7fff3bba 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,7 +35,7 @@ print(response.value) # returns unit-bearing values thanks to Pint print(response.value.magnitude) # or simple floats ``` -OBD connections operate in a request-reply fashion. To retrieve data from the car, you must send commands that query for the data you want (e.g. RPM, Vehicle speed, etc). In python-OBD, this is done with the `query()` function. The commands themselves are represented as objects, and can be looked up by name or value in `obd.commands`. The `query()` function will return a response object with parsed data in its `value` and `unit` properties. +OBD connections operate in a request-reply fashion. To retrieve data from the car, you must send commands that query for the data you want (e.g. RPM, Vehicle speed, etc). In python-OBD, this is done with the `query()` function. The commands themselves are represented as objects, and can be looked up by name or value in `obd.commands`. The `query()` function will return a response object with parsed data in its `value` property.
From b90d27475883d3219f8c7b1a2d3523cfbd4035ba Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 18:40:25 -0400 Subject: [PATCH 447/569] responce --> response --- docs/Responses.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/Responses.md b/docs/Responses.md index 0048d8fe..6e4f37ce 100644 --- a/docs/Responses.md +++ b/docs/Responses.md @@ -76,7 +76,7 @@ The status command returns information about the Malfunction Indicator Light (ch ```python response.value.MIL # boolean for whether the check-engine is lit response.value.DTC_count # number (int) of DTCs being thrown -responce.value.ignition_type # "spark" or "compression" +response.value.ignition_type # "spark" or "compression" ``` The status command also provides information regarding the availability and status of various system tests. These are exposed as `StatusTest` objects, loaded into named properties. Each test object has boolean flags for its availability and completion. @@ -115,14 +115,14 @@ Each DTC is represented by a tuple containing the DTC code, and a description (i ```python # obd.commands.GET_DTC -responce.value = [ +response.value = [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ("B0003", ""), # unknown error code, it's probably vehicle-specific ("C0123", "") ] # obd.commands.FREEZE_DTC -responce.value = ("P0104", "Mass or Volume Air Flow Circuit Intermittent") +response.value = ("P0104", "Mass or Volume Air Flow Circuit Intermittent") ``` --- @@ -161,14 +161,14 @@ Returns a 2D structure of tuples (representing bank and sensor number), that hol ```python # obd.commands.O2_SENSORS -responce.value = ( +response.value = ( (), # bank 0 is invalid, this is merely for correct indexing (True, True, True, False), # bank 1 (False, False, False, False) # bank 2 ) # obd.commands.O2_SENSORS_ALT -responce.value = ( +response.value = ( (), # bank 0 is invalid, this is merely for correct indexing (True, True), # bank 1 (True, False), # bank 2 From a81c5b22248c1b11acec1c9db78370201d4c2edb Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 18:41:37 -0400 Subject: [PATCH 448/569] reorganized module layout --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 7fff3bba..3ad138b3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -48,11 +48,11 @@ obd.OBD # main OBD connection class obd.Async # asynchronous OBD connection class obd.commands # command tables obd.Unit # unit tables (a Pint UnitRegistry) -obd.logger # the OBD module's root logger (for debug) obd.OBDStatus # enum for connection status obd.scan_serial # util function for manually scanning for OBD adapters obd.OBDCommand # class for making your own OBD Commands obd.ECU # enum for marking which ECU a command should listen to +obd.logger # the OBD module's root logger (for debug) ```
From 96ac20d00bd3b1ab02ba7794e05ffb323945e550 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 18:47:02 -0400 Subject: [PATCH 449/569] more links and edits --- docs/Connections.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Connections.md b/docs/Connections.md index 749e05ea..04c249a6 100644 --- a/docs/Connections.md +++ b/docs/Connections.md @@ -20,13 +20,13 @@ connection = obd.OBD(ports[0]) # connect to the first port in the list
-### OBD(portstr=None, baudrate=38400, protocol=None, fast=True): +### OBD(portstr=None, baudrate=None, protocol=None, fast=True): `portstr`: The UNIX device file or Windows COM Port for your adapter. The default value (`None`) will auto select a port. -`baudrate`: The baudrate at which to set the serial connection. This can vary from adapter to adapter. Typical values are: 9600, 38400, 19200, 57600, 115200 +`baudrate`: The baudrate at which to set the serial connection. This can vary from adapter to adapter. Typical values are: 9600, 38400, 19200, 57600, 115200. The default value (`None`) will auto select a baudrate. -`protocol`: Forces python-OBD to use the given protocol when communicating with the adapter. See `protocol_id()` for possible values. The default value (`None`) will auto select a protocol. +`protocol`: Forces python-OBD to use the given protocol when communicating with the adapter. See [protocol_id()](Connections.md/#protocol_id) for possible values. The default value (`None`) will auto select a protocol. `fast`: Allows commands to be optimized before being sent to the car. Python-OBD currently makes two such optimizations: @@ -41,7 +41,7 @@ Disabling fast mode will guarantee that python-OBD outputs the unaltered command ### query(command, force=False) -Sends an `OBDCommand` to the car, and returns a `OBDResponse` object. This function will block until a response is received from the car. This function will also check whether the given command is supported by your car. If a command is not marked as supported, it will not be sent to the car, and an empty `Response` will be returned. To force an unsupported command to be sent, there is an optional `force` parameter for your convenience. +Sends an `OBDCommand` to the car, and returns an `OBDResponse` object. This function will block until a response is received from the car. This function will also check whether the given command is supported by your car. If a command is not marked as supported, it will not be sent, and an empty `OBDResponse` will be returned. To force an unsupported command to be sent, there is an optional `force` parameter for your convenience. *For non-blocking querying, see [Async Querying](Async Connections.md)* From a709860b8fe0309ee6586a2b01c825bbb6f63ca1 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 19:20:05 -0400 Subject: [PATCH 450/569] added example for adding support for OBDCommands --- docs/Connections.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/Connections.md b/docs/Connections.md index 04c249a6..c5d64402 100644 --- a/docs/Connections.md +++ b/docs/Connections.md @@ -142,8 +142,17 @@ Closes the connection. ### supported_commands -Property containing a list of commands that are supported by the car. +Property containing a `set` of commands that are supported by the car. +If you wish to manually mark a command as supported (prevents having to use `query(force=True)`), add the command to this set. This is not necessary when using python-OBD's builtin commands, but is useful if you create [custom commands](Custom Commands.md). + +```python +import obd +connection = obd.OBD() + +# manually mark the given command as supported +connection.supported_commands.add() +``` ---
From 2a5e9bd20d24d7a16072ab0f2ded11131ec00754 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 19:25:22 -0400 Subject: [PATCH 451/569] replaced retry with higher timeout --- obd/elm327.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 343420d3..ad058029 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -117,7 +117,7 @@ def __init__(self, portname, baudrate, protocol): parity = serial.PARITY_NONE, \ stopbits = 1, \ bytesize = 8, - timeout = 3) # seconds + timeout = 10) # seconds logger.info("Serial port successfully opened on " + self.port_name()) except serial.SerialException as e: @@ -429,7 +429,6 @@ def __read(self): logger.info("cannot perform __read() when unconnected") return "" - attempts = 2 buffer = bytearray() while True: @@ -438,14 +437,8 @@ def __read(self): # if nothing was recieved if not data: - - if attempts <= 0: - logger.warning("Failed to read port, giving up") - break - - logger.info("Failed to read port, trying again...") - attempts -= 1 - continue + logger.warning("Failed to read port") + break buffer.extend(data) From 7f2488c733f5832d5a210a7560a8a11a3cf5bd02 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 19:59:58 -0400 Subject: [PATCH 452/569] altered basic usage, placed red warnings for backwards compat and below 1.0.0 --- README.md | 4 ++-- docs/Commands.md | 2 ++ docs/Responses.md | 4 +++- docs/index.md | 6 ++++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 11862c8c..bf54aa1c 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,12 @@ import obd connection = obd.OBD() # auto-connects to USB or RF port -cmd = obd.commands.RPM # select an OBD command (sensor) +cmd = obd.commands.SPEED # select an OBD command (sensor) response = connection.query(cmd) # send the command, and parse the response print(response.value) # returns unit-bearing values thanks to Pint -print(response.value.magnitude) # or simple floats +print(response.value.to("mph")) # user-friendly unit conversions ``` Documentation diff --git a/docs/Commands.md b/docs/Commands.md index fbd54eae..599fdd8b 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -208,6 +208,8 @@ Mode 03 contains a single command `GET_DTC` which requests all diagnostic troubl # Mode 06 +*WARNING: mode 06 is experimental. While it passes software tests, it has not been tested on a real vehicle. Any debug output for this mode would be greatly appreciated.* + Mode 06 commands are used to monitor various test results from the vehicle. All commands in this mode return the same datatype, as described in the [Monitor Response](Responses.md#monitors-mode-06-responses) section. Currently, mode 06 commands are only implemented for CAN protocols (ISO 15765-4). |PID | Name | Description | Response Value | diff --git a/docs/Responses.md b/docs/Responses.md index 6e4f37ce..802e924a 100644 --- a/docs/Responses.md +++ b/docs/Responses.md @@ -27,10 +27,12 @@ if not r.is_null(): # Pint Values -The `value` property typically contains a [Pint](http://pint.readthedocs.io/en/latest/) `Quantity` object, but can also hold complex structures (depending on the request). Pint quantities combine a value and unit into a single class, and are used to represent physical values (such as "4 seconds", and "88 mph"). This allows for consistency when doing math and unit conversions. Pint maintains a registry of units, which is exposed in python-OBD as `obd.Unit`. +The `value` property typically contains a [Pint](http://pint.readthedocs.io/en/latest/) `Quantity` object, but can also hold complex structures (depending on the request). Pint quantities combine a value and unit into a single class, and are used to represent physical values such as "4 seconds", and "88 mph". This allows for consistency when doing math and unit conversions. Pint maintains a registry of units, which is exposed in python-OBD as `obd.Unit`. Below are common operations that can be done with Pint units and quantities. For more information, check out the [Pint Documentation](http://pint.readthedocs.io/en/latest/). +*NOTE: for backwards compatibility with previous versions of python-OBD, use `response.value.magnitude` in place of `response.value`* + ```python import obd diff --git a/docs/index.md b/docs/index.md index 3ad138b3..7688edea 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,6 +2,8 @@ Python-OBD is a library for handling data from a car's [**O**n-**B**oard **D**iagnostics port](https://en.wikipedia.org/wiki/On-board_diagnostics) (OBD-II). It can stream real time sensor data, perform diagnostics (such as reading check-engine codes), and is fit for the Raspberry Pi. This library is designed to work with standard [ELM327 OBD-II adapters](http://www.amazon.com/s/ref=nb_sb_noss?field-keywords=elm327). +*NOTE: Python-OBD is below 1.0.0, meaning the API may change between minor versions. Consult the [GitHub release page](https://github.com/brendan-w/python-OBD/releases) for changelogs before updating.* +
# Installation @@ -27,12 +29,12 @@ import obd connection = obd.OBD() # auto-connects to USB or RF port -cmd = obd.commands.RPM # select an OBD command (sensor) +cmd = obd.commands.SPEED # select an OBD command (sensor) response = connection.query(cmd) # send the command, and parse the response print(response.value) # returns unit-bearing values thanks to Pint -print(response.value.magnitude) # or simple floats +print(response.value.to("mph")) # user-friendly unit conversions ``` OBD connections operate in a request-reply fashion. To retrieve data from the car, you must send commands that query for the data you want (e.g. RPM, Vehicle speed, etc). In python-OBD, this is done with the `query()` function. The commands themselves are represented as objects, and can be looked up by name or value in `obd.commands`. The `query()` function will return a response object with parsed data in its `value` property. From 9cd77c90d7a5c1045ef4f4c832dee678fdc777ed Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 20:23:40 -0400 Subject: [PATCH 453/569] bytearray now --- obd/protocols/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/obd/protocols/README.md b/obd/protocols/README.md index 7f6a250f..e5b9c193 100644 --- a/obd/protocols/README.md +++ b/obd/protocols/README.md @@ -1,9 +1,9 @@ Notes ----- -Each protocol object is callable, and accepts a list of raw input strings, and returns a list of parsed `Message` objects. The `data` field will contain a list of integers, corresponding to all relevant data returned by the command. +Each protocol object is callable, and accepts a list of raw input strings, and returns a list of parsed `Message` objects. The `data` field will contain a bytearray, corresponding to all relevant data returned by the command. -*Note: `Message.data` does not refer to the full data field of a message. Things like PCI/Mode/PID bytes are removed. If you want to see these fields, use `Frame.data` for the full (per-spec) data field.* +*Note: `Message.data` does not refer to the full data field of a message. Things like PCI/Mode/PID bytes are often removed. If you want to see these fields, use `Frame.data` for the full (per-spec) data field.* For example, these are the resultant `Message.data` fields for some single frame messages: @@ -15,7 +15,7 @@ A CAN Message: A J1850 Message: 48 6B 10 41 00 BE 7F B8 13 FF [ data ] -``` +``` The parsing itself (invoking `__call__`) is stateless. The only stateful part of a `Protocol` is the `ECU_Map`. These objects correlate OBD transmitter IDs (`tx_id`'s) with the various ECUs in the car. This way, `Message` objects can be marked with ECU constants such as: From 5574775362365b365d5224c84b40178e32f8d64d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 21:49:34 -0400 Subject: [PATCH 454/569] deactivated mode 9 commands until they're implemented --- obd/commands.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 86dcf94e..9cb9ecfb 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -286,9 +286,9 @@ __mode9__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("PIDS_9A" , "Supported PIDs [01-20]" , b"0900", 4, pid, ECU.ENGINE, True), - OBDCommand("VIN_MESSAGE_COUNT" , "VIN Message Count" , b"0901", 1, uas(0x01), ECU.ENGINE, True), - OBDCommand("VIN" , "Get Vehicle Identification Number" , b"0902", 20, raw_string, ECU.ENGINE, True), + # OBDCommand("PIDS_9A" , "Supported PIDs [01-20]" , b"0900", 4, pid, ECU.ENGINE, True), + # OBDCommand("VIN_MESSAGE_COUNT" , "VIN Message Count" , b"0901", 1, uas(0x01), ECU.ENGINE, True), + # OBDCommand("VIN" , "Get Vehicle Identification Number" , b"0902", 20, raw_string, ECU.ENGINE, True), ] __misc__ = [ From 0279782b6e9a060408709325a570db4caa68d3f7 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 22:13:53 -0400 Subject: [PATCH 455/569] enable STATUS_DRIVE_CYCLE --- docs/Commands.md | 2 +- obd/commands.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Commands.md b/docs/Commands.md index 599fdd8b..1b6b1ae0 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -139,7 +139,7 @@ obd.commands.has_pid(1, 12) # True | 3E | CATALYST_TEMP_B1S2 | Catalyst Temperature: Bank 1 - Sensor 2 | Unit.celsius | | 3F | CATALYST_TEMP_B2S2 | Catalyst Temperature: Bank 2 - Sensor 2 | Unit.celsius | | 40 | PIDS_C | Supported PIDs [41-60] | bitarray | -| 41 | *unsupported* | *unsupported* | | +| 41 | STATUS_DRIVE_CYCLE | Monitor status this drive cycle | [special](Responses.md#status) | | 42 | *unsupported* | *unsupported* | | | 43 | *unsupported* | *unsupported* | | | 44 | *unsupported* | *unsupported* | | diff --git a/obd/commands.py b/obd/commands.py index 9cb9ecfb..d5df3f2a 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -119,7 +119,7 @@ # name description cmd bytes decoder ECU fast OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , b"0140", 4, pid, ECU.ENGINE, True), - OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , b"0141", 4, drop, ECU.ENGINE, True), + OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , b"0141", 4, status, ECU.ENGINE, True), OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , b"0142", 2, drop, ECU.ENGINE, True), OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , b"0143", 2, drop, ECU.ENGINE, True), OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , b"0144", 2, drop, ECU.ENGINE, True), From 8e7a7c168e914257cb45a58c6a07bf6240b0dda1 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 22:16:53 -0400 Subject: [PATCH 456/569] enable CONTROL_MODULE_VOLTAGE --- docs/Commands.md | 2 +- obd/commands.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Commands.md b/docs/Commands.md index 1b6b1ae0..e9634c7b 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -140,7 +140,7 @@ obd.commands.has_pid(1, 12) # True | 3F | CATALYST_TEMP_B2S2 | Catalyst Temperature: Bank 2 - Sensor 2 | Unit.celsius | | 40 | PIDS_C | Supported PIDs [41-60] | bitarray | | 41 | STATUS_DRIVE_CYCLE | Monitor status this drive cycle | [special](Responses.md#status) | -| 42 | *unsupported* | *unsupported* | | +| 42 | CONTROL_MODULE_VOLTAGE | Control module voltage | Unit.volt | | 43 | *unsupported* | *unsupported* | | | 44 | *unsupported* | *unsupported* | | | 45 | RELATIVE_THROTTLE_POS | Relative throttle position | Unit.percent | diff --git a/obd/commands.py b/obd/commands.py index d5df3f2a..1f7b3c69 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -120,7 +120,7 @@ # name description cmd bytes decoder ECU fast OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , b"0140", 4, pid, ECU.ENGINE, True), OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , b"0141", 4, status, ECU.ENGINE, True), - OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , b"0142", 2, drop, ECU.ENGINE, True), + OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , b"0142", 2, uas(0x0B), ECU.ENGINE, True), OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , b"0143", 2, drop, ECU.ENGINE, True), OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , b"0144", 2, drop, ECU.ENGINE, True), OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , b"0145", 1, percent, ECU.ENGINE, True), From aa9f8c1126591b6ec881717aa2ff6f3ddeef71e5 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 22:22:01 -0400 Subject: [PATCH 457/569] enable COMMANDED_EQUIV_RATIO --- docs/Commands.md | 2 +- obd/commands.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Commands.md b/docs/Commands.md index e9634c7b..9e67788b 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -142,7 +142,7 @@ obd.commands.has_pid(1, 12) # True | 41 | STATUS_DRIVE_CYCLE | Monitor status this drive cycle | [special](Responses.md#status) | | 42 | CONTROL_MODULE_VOLTAGE | Control module voltage | Unit.volt | | 43 | *unsupported* | *unsupported* | | -| 44 | *unsupported* | *unsupported* | | +| 44 | COMMANDED_EQUIV_RATIO | Commanded equivalence ratio | Unit.ratio | | 45 | RELATIVE_THROTTLE_POS | Relative throttle position | Unit.percent | | 46 | AMBIANT_AIR_TEMP | Ambient air temperature | Unit.celsius | | 47 | THROTTLE_POS_B | Absolute throttle position B | Unit.percent | diff --git a/obd/commands.py b/obd/commands.py index 1f7b3c69..e45a06eb 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -122,7 +122,7 @@ OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , b"0141", 4, status, ECU.ENGINE, True), OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , b"0142", 2, uas(0x0B), ECU.ENGINE, True), OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , b"0143", 2, drop, ECU.ENGINE, True), - OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , b"0144", 2, drop, ECU.ENGINE, True), + OBDCommand("COMMANDED_EQUIV_RATIO" , "Commanded equivalence ratio" , b"0144", 2, uas(0x1E), ECU.ENGINE, True), OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , b"0145", 1, percent, ECU.ENGINE, True), OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , b"0146", 1, temp, ECU.ENGINE, True), OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , b"0147", 1, percent, ECU.ENGINE, True), From 40f16d5704008099cb7163b7f032658b2cd5eabb Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 6 Jul 2016 22:30:02 -0400 Subject: [PATCH 458/569] enable ABSOLUTE_LOAD --- docs/Commands.md | 2 +- obd/commands.py | 2 +- obd/decoders.py | 7 +++++++ tests/test_decoders.py | 4 ++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/Commands.md b/docs/Commands.md index 9e67788b..c799bcc7 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -141,7 +141,7 @@ obd.commands.has_pid(1, 12) # True | 40 | PIDS_C | Supported PIDs [41-60] | bitarray | | 41 | STATUS_DRIVE_CYCLE | Monitor status this drive cycle | [special](Responses.md#status) | | 42 | CONTROL_MODULE_VOLTAGE | Control module voltage | Unit.volt | -| 43 | *unsupported* | *unsupported* | | +| 43 | ABSOLUTE_LOAD | Absolute load value | Unit.percent | | 44 | COMMANDED_EQUIV_RATIO | Commanded equivalence ratio | Unit.ratio | | 45 | RELATIVE_THROTTLE_POS | Relative throttle position | Unit.percent | | 46 | AMBIANT_AIR_TEMP | Ambient air temperature | Unit.celsius | diff --git a/obd/commands.py b/obd/commands.py index e45a06eb..b5735ce2 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -121,7 +121,7 @@ OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , b"0140", 4, pid, ECU.ENGINE, True), OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , b"0141", 4, status, ECU.ENGINE, True), OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , b"0142", 2, uas(0x0B), ECU.ENGINE, True), - OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , b"0143", 2, drop, ECU.ENGINE, True), + OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , b"0143", 2, absolute_load, ECU.ENGINE, True), OBDCommand("COMMANDED_EQUIV_RATIO" , "Commanded equivalence ratio" , b"0144", 2, uas(0x1E), ECU.ENGINE, True), OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , b"0145", 1, percent, ECU.ENGINE, True), OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , b"0146", 1, temp, ECU.ENGINE, True), diff --git a/obd/decoders.py b/obd/decoders.py index f12ff41a..ce7b31c8 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -222,6 +222,13 @@ def o2_sensors_alt(messages): tuple(bits[6:]), # bank 4 ) +# 0 to 25700 % +def absolute_load(messages): + d = messages[0].data + v = bytes_to_int(d) + v *= 100.0 / 255.0 + return v * Unit.percent + def elm_voltage(messages): # doesn't register as a normal OBD response, # so access the raw frame data diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 8c5e6977..5ef0f238 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -145,6 +145,10 @@ def test_aux_input_status(): assert d.aux_input_status(m("00")) == False assert d.aux_input_status(m("80")) == True +def test_absolute_load(): + assert d.absolute_load(m("0000")) == 0 * Unit.percent + assert d.absolute_load(m("FFFF")) == 25700 * Unit.percent + def test_elm_voltage(): # these aren't parsed as standard hex messages, so manufacture our own assert d.elm_voltage([ Message([ Frame("12.875") ]) ]) == 12.875 * Unit.volt From 9a147fceb90af7a98d70ea9d737efec53429b0a9 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 7 Jul 2016 01:20:17 -0400 Subject: [PATCH 459/569] fixed copy/paste typo --- docs/Responses.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Responses.md b/docs/Responses.md index 802e924a..30b992e6 100644 --- a/docs/Responses.md +++ b/docs/Responses.md @@ -174,8 +174,8 @@ response.value = ( (), # bank 0 is invalid, this is merely for correct indexing (True, True), # bank 1 (True, False), # bank 2 - (False, False), # bank 2 - (False, False) # bank 2 + (False, False), # bank 3 + (False, False) # bank 4 ) # example usage: From cc8ce34c3c2065e26655082f4b35e501096a54aa Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 7 Jul 2016 14:49:39 -0400 Subject: [PATCH 460/569] added mkdocs patch for RTFD --- docs/assets/extra.js | 48 ++++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 18 +++++++++-------- 2 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 docs/assets/extra.js diff --git a/docs/assets/extra.js b/docs/assets/extra.js new file mode 100644 index 00000000..5c927480 --- /dev/null +++ b/docs/assets/extra.js @@ -0,0 +1,48 @@ + +$(document).ready(function () { + fixSearch(); +}); + +/* + * RTD messes up MkDocs' search feature by tinkering with the search box defined in the theme, see + * https://github.com/rtfd/readthedocs.org/issues/1088. This function sets up a DOM4 MutationObserver + * to react to changes to the search form (triggered by RTD on doc ready). It then reverts everything + * the RTD JS code modified. + */ + +function fixSearch() +{ + var target = document.getElementById('rtd-search-form'); + var config = {attributes: true, childList: true}; + + var observer = new MutationObserver(function(mutations) { + // if it isn't disconnected it'll loop infinitely because the observed element is modified + observer.disconnect(); + var form = $('#rtd-search-form'); + form.empty(); + form.attr('action', 'https://' + window.location.hostname + '/en/' + determineSelectedBranch() + '/search.html'); + $('').attr({ + type: "text", + name: "q", + placeholder: "Search docs" + }).appendTo(form); + }); + + // don't run this outside RTD hosting + if (window.location.origin.indexOf('readthedocs') > -1) + { + observer.observe(target, config); + } +} + +function determineSelectedBranch() +{ + var branch = 'dev', path = window.location.pathname; + if (window.location.origin.indexOf('readthedocs') > -1) + { + // path is like /en///build/ -> extract 'lang' + // split[0] is an '' because the path starts with the separator + branch = path.split('/')[2]; + } + return branch; +} diff --git a/mkdocs.yml b/mkdocs.yml index d0800f49..186f92cf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,14 +1,16 @@ site_name: python-OBD repo_url: https://github.com/brendan-w/python-OBD repo_name: GitHub +extra_javascript: + - assets/extra.js pages: -- 'Getting Started': 'index.md' -- 'OBD Connections': 'Connections.md' -- 'Commands': 'Commands.md' -- 'Responses': 'Responses.md' -- 'Async Connections': 'Async Connections.md' -- 'Custom Commands': 'Custom Commands.md' -- 'Debug': 'Debug.md' -- 'Troubleshooting': 'Troubleshooting.md' + - 'Getting Started': 'index.md' + - 'OBD Connections': 'Connections.md' + - 'Commands': 'Commands.md' + - 'Responses': 'Responses.md' + - 'Async Connections': 'Async Connections.md' + - 'Custom Commands': 'Custom Commands.md' + - 'Debug': 'Debug.md' + - 'Troubleshooting': 'Troubleshooting.md' theme: readthedocs From b491a17263cab0e35f11f4dcb241bb9338679346 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 11 Jul 2016 13:59:57 -0400 Subject: [PATCH 461/569] return empty list instead of empty string --- obd/elm327.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/elm327.py b/obd/elm327.py index ad058029..db35f2e7 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -427,7 +427,7 @@ def __read(self): """ if not self.__port: logger.info("cannot perform __read() when unconnected") - return "" + return [] buffer = bytearray() From b120b81a978ff84da99a1e62b648f54f1a054ad2 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 11 Jul 2016 14:00:29 -0400 Subject: [PATCH 462/569] port_name now returns empty strings when no connection --- docs/Connections.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Connections.md b/docs/Connections.md index c5d64402..a6dbfa94 100644 --- a/docs/Connections.md +++ b/docs/Connections.md @@ -12,7 +12,7 @@ connection = obd.OBD("/dev/ttyUSB0") # create connection with USB 0 # OR -ports = obd.scan_serial() # return list of valid USB or RF ports +ports = obd.scan_serial() # return list of valid USB or RF ports print ports # ['/dev/ttyUSB0', '/dev/ttyUSB1'] connection = obd.OBD(ports[0]) # connect to the first port in the list ``` @@ -87,7 +87,7 @@ connection.status() == OBDStatus.CAR_CONNECTED ### port_name() -Returns the string name for the currently connected port (`"/dev/ttyUSB0"`). If no connection was made, this function returns `"Not connected to any port"`. +Returns the string name for the currently connected port (`"/dev/ttyUSB0"`). If no connection was made, this function returns an empty string. --- From 9b927154797ab965aec1ce58e18b6b4837a43048 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 11 Jul 2016 14:22:33 -0400 Subject: [PATCH 463/569] added CAN txid for the transmission --- obd/protocols/protocol_can.py | 1 + 1 file changed, 1 insertion(+) diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 8d4b2191..e2381f79 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -41,6 +41,7 @@ class CANProtocol(Protocol): TX_ID_ENGINE = 0 + TX_ID_TRANSMISSION = 1 FRAME_TYPE_SF = 0x00 # single frame FRAME_TYPE_FF = 0x10 # first frame of multi-frame message From 7261a3520849b8c48438df176570180f55445a94 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 21 Jul 2016 15:46:23 -0400 Subject: [PATCH 464/569] moved command tables into their own page --- docs/Command Lookup.md | 59 +++++++++++++++++++++++ docs/{Commands.md => Command Tables.md} | 63 ------------------------- mkdocs.yml | 19 ++++---- 3 files changed, 69 insertions(+), 72 deletions(-) create mode 100644 docs/Command Lookup.md rename docs/{Commands.md => Command Tables.md} (95%) diff --git a/docs/Command Lookup.md b/docs/Command Lookup.md new file mode 100644 index 00000000..43e9ace7 --- /dev/null +++ b/docs/Command Lookup.md @@ -0,0 +1,59 @@ +`OBDCommand`s are objects used to query information from the vehicle. They contain all of the information neccessary to perform the query, and decode the cars response. Python-OBD has [built in tables](Command Tables.md) for the most common commands. They can be looked up by name, or by mode & PID. + +```python +import obd + +c = obd.commands.RPM + +# OR + +c = obd.commands['RPM'] + +# OR + +c = obd.commands[1][12] # mode 1, PID 12 (RPM) +``` + +The `commands` table also has a few helper methods for determining if a particular name or PID is present. + +--- + +### has_command(command) + +Checks the internal command tables for the existance of the given `OBDCommand` object. Commands are compared by mode and PID value. + +```python +import obd +obd.commands.has_command(obd.commands.RPM) # True +``` + +--- + +### has_name(name) + +Checks the internal command tables for a command with the given name. This is also the function of the `in` operator. + +```python +import obd + +obd.commands.has_name('RPM') # True + +# OR + +'RPM' in obd.commands # True +``` + +--- + +### has_pid(mode, pid) + +Checks the internal command tables for a command with the given mode and PID. + +```python +import obd +obd.commands.has_pid(1, 12) # True +``` + +--- + +
diff --git a/docs/Commands.md b/docs/Command Tables.md similarity index 95% rename from docs/Commands.md rename to docs/Command Tables.md index c799bcc7..f48ffa1a 100644 --- a/docs/Commands.md +++ b/docs/Command Tables.md @@ -1,66 +1,3 @@ - -# Lookup - -`OBDCommand`s are objects used to query information from the vehicle. They contain all of the information neccessary to perform the query, and decode the cars response. Python-OBD has built in tables for the most common commands. They can be looked up by name, or by mode & PID. - -```python -import obd - -c = obd.commands.RPM - -# OR - -c = obd.commands['RPM'] - -# OR - -c = obd.commands[1][12] # mode 1, PID 12 (RPM) -``` - -The `commands` table also has a few helper methods for determining if a particular name or PID is present. - ---- - -### has_command(command) - -Checks the internal command tables for the existance of the given `OBDCommand` object. Commands are compared by mode and PID value. - -```python -import obd -obd.commands.has_command(obd.commands.RPM) # True -``` - ---- - -### has_name(name) - -Checks the internal command tables for a command with the given name. This is also the function of the `in` operator. - -```python -import obd - -obd.commands.has_name('RPM') # True - -# OR - -'RPM' in obd.commands # True -``` - ---- - -### has_pid(mode, pid) - -Checks the internal command tables for a command with the given mode and PID. - -```python -import obd -obd.commands.has_pid(1, 12) # True -``` - ---- - -
- # OBD-II adapter (ELM327 commands) |PID | Name | Description | Response Value | diff --git a/mkdocs.yml b/mkdocs.yml index 186f92cf..bec2ac65 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,15 +2,16 @@ site_name: python-OBD repo_url: https://github.com/brendan-w/python-OBD repo_name: GitHub extra_javascript: - - assets/extra.js +- assets/extra.js pages: - - 'Getting Started': 'index.md' - - 'OBD Connections': 'Connections.md' - - 'Commands': 'Commands.md' - - 'Responses': 'Responses.md' - - 'Async Connections': 'Async Connections.md' - - 'Custom Commands': 'Custom Commands.md' - - 'Debug': 'Debug.md' - - 'Troubleshooting': 'Troubleshooting.md' +- 'Getting Started': 'index.md' +- 'OBD Connections': 'Connections.md' +- 'Command Lookup': 'Command Lookup.md' +- 'Command Tables' : 'Command Tables.md' +- 'Responses': 'Responses.md' +- 'Async Connections': 'Async Connections.md' +- 'Custom Commands': 'Custom Commands.md' +- 'Debug': 'Debug.md' +- 'Troubleshooting': 'Troubleshooting.md' theme: readthedocs From 237f89f034b858d11574ddfedd4a91039d460947 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 22 Jul 2016 14:19:23 -0400 Subject: [PATCH 465/569] enable ECU tagging for the transmission --- obd/protocols/protocol.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 2dce7383..2f925e31 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -127,6 +127,7 @@ class Protocol(object): # the TX_IDs of known ECUs TX_ID_ENGINE = None + TX_ID_TRANSMISSION = None def __init__(self, lines_0100): @@ -253,12 +254,16 @@ def populate_ecu_map(self, messages): # if any tx_ids are exact matches to the expected values, record them for m in messages: + if m.tx_id is None: + logger.debug("parse_frame failed to extract TX_ID") + continue + if m.tx_id == self.TX_ID_ENGINE: self.ecu_map[m.tx_id] = ECU.ENGINE found_engine = True + elif m.tx_id == self.TX_ID_TRANSMISSION: + self.ecu_map[m.tx_id] = ECU.TRANSMISSION # TODO: program more of these when we figure out their constants - # elif m.tx_id == self.TX_ID_TRANSMISSION: - # self.ecu_map[m.tx_id] = ECU.TRANSMISSION if not found_engine: # last resort solution, choose ECU with the most bits set From d76dae375134c2e5490ce70420e655c9e6de4251 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 22 Jul 2016 14:34:22 -0400 Subject: [PATCH 466/569] more debug info --- obd/elm327.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index db35f2e7..357388bc 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -118,7 +118,7 @@ def __init__(self, portname, baudrate, protocol): stopbits = 1, \ bytesize = 8, timeout = 10) # seconds - logger.info("Serial port successfully opened on " + self.port_name()) + logger.info("Port opened") except serial.SerialException as e: self.__error(e) @@ -183,6 +183,8 @@ def set_protocol(self, protocol): def manual_protocol(self, protocol): + logger.debug("Setting fixed protocol: %s" % protocol) + r = self.__send(b"ATTP" + protocol.encode()) r0100 = self.__send(b"0100") @@ -204,6 +206,8 @@ def auto_protocol(self): and this function returns True """ + logger.debug("Choosing protocol automatically") + # -------------- try the ELM's auto protocol mode -------------- r = self.__send(b"ATSP0") @@ -249,10 +253,12 @@ def set_baudrate(self, baud): if baud is None: # when connecting to pseudo terminal, don't bother with auto baud if self.port_name().startswith("/dev/pts"): + logger.debug("Detected pseudo terminal, skipping baudrate setup") return True else: return self.auto_baudrate() else: + logger.debug("Setting fixed baudrate: %d" % baud) self.__port.baudrate = baud return True @@ -315,14 +321,10 @@ def __has_message(self, lines, text): return False - def __error(self, msg=None): + def __error(self, msg): """ handles fatal failures, print logger.info info and closes serial """ - self.close() - - logger.error("Connection Error:") - if msg is not None: - logger.error(str(msg)) + logger.error(str(msg)) def port_name(self): From a47ade52d1fa2e11ecc63f662313b641d864e731 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 22 Jul 2016 15:06:01 -0400 Subject: [PATCH 467/569] more debug tweaking --- obd/elm327.py | 29 +++++++++++++++-------------- obd/obd.py | 5 +++-- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 357388bc..7bc776bb 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -105,6 +105,13 @@ class ELM327: def __init__(self, portname, baudrate, protocol): """Initializes port by resetting device and gettings supported PIDs. """ + logger.info("Initializing ELM327: PORT=%s BAUD=%s PROTOCOL=%s" % + ( + portname, + "auto" if baudrate is None else baudrate, + "auto" if protocol is None else protocol, + )) + self.__status = OBDStatus.NOT_CONNECTED self.__port = None self.__protocol = UnknownProtocol([]) @@ -112,14 +119,11 @@ def __init__(self, portname, baudrate, protocol): # ------------- open port ------------- try: - logger.info("Opening serial port '%s'" % portname) self.__port = serial.Serial(portname, \ parity = serial.PARITY_NONE, \ stopbits = 1, \ bytesize = 8, timeout = 10) # seconds - logger.info("Port opened") - except serial.SerialException as e: self.__error(e) return @@ -164,7 +168,12 @@ def __init__(self, portname, baudrate, protocol): # try to communicate with the car, and load the correct protocol parser if self.set_protocol(protocol): self.__status = OBDStatus.CAR_CONNECTED - logger.info("Connection successful") + logger.info("Connected Successfully: PORT=%s BAUD=%s PROTOCOL=%s" % + ( + portname, + self.__protocol.ELM_ID, + self.__port.baudrate, + )) else: logger.error("Connected to the adapter, but failed to connect to the vehicle") @@ -182,9 +191,6 @@ def set_protocol(self, protocol): def manual_protocol(self, protocol): - - logger.debug("Setting fixed protocol: %s" % protocol) - r = self.__send(b"ATTP" + protocol.encode()) r0100 = self.__send(b"0100") @@ -206,8 +212,6 @@ def auto_protocol(self): and this function returns True """ - logger.debug("Choosing protocol automatically") - # -------------- try the ELM's auto protocol mode -------------- r = self.__send(b"ATSP0") @@ -234,7 +238,7 @@ def auto_protocol(self): # an unknown protocol # this is likely because not all adapter/car combinations work # in "auto" mode. Some respond to ATDPN responded with "0" - logger.info("ELM responded with unknown protocol. Trying them one-by-one") + logger.debug("ELM responded with unknown protocol. Trying them one-by-one") for p in self._TRY_PROTOCOL_ORDER: r = self.__send(b"ATTP" + p.encode()) @@ -258,7 +262,6 @@ def set_baudrate(self, baud): else: return self.auto_baudrate() else: - logger.debug("Setting fixed baudrate: %d" % baud) self.__port.baudrate = baud return True @@ -269,8 +272,6 @@ def auto_baudrate(self): Returns boolean for success. """ - logger.debug("Choosing baudrate automatically") - # before we change the timout, save the "normal" value timeout = self.__port.timeout self.__port.timeout = 0.1 # we're only talking with the ELM, so things should go quickly @@ -399,7 +400,7 @@ def __send(self, cmd, delay=None): self.__write(cmd) if delay is not None: - logger.info("wait: %d seconds" % delay) + logger.debug("wait: %d seconds" % delay) time.sleep(delay) return self.__read() diff --git a/obd/obd.py b/obd/obd.py index 654dcf5b..e3ad3732 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -99,7 +99,7 @@ def __load_commands(self): logger.warning("Cannot load commands: No connection to car") return - logger.info("querying for supported PIDs (commands)...") + logger.info("querying for supported commands") pid_getters = commands.pid_getters() for get in pid_getters: # PID listing commands should sequentialy become supported @@ -109,9 +109,10 @@ def __load_commands(self): # when querying, only use the blocking OBD.query() # prevents problems when query is redefined in a subclass (like Async) - response = OBD.query(self, get, force=True) # ask nicely + response = OBD.query(self, get) if response.is_null(): + logger.info("No valid data for PID listing command: %s" % get) continue # loop through PIDs bitarray From 5f1c9aa908f7855680928d302ee6311910f2bffb Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 22 Jul 2016 15:10:03 -0400 Subject: [PATCH 468/569] prettier ECU map debug output --- obd/protocols/protocol.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 2f925e31..c8e7ab71 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -153,7 +153,8 @@ def __init__(self, lines_0100): # log out the ecu map for tx_id, ecu in self.ecu_map.items(): names = [k for k in ECU.__dict__ if ECU.__dict__[k] == ecu ] - logger.debug("Chose ECU %d as %s" % (tx_id, names)) + names = ", ".join(names) + logger.debug("map ECU %d --> %s" % (tx_id, names)) def __call__(self, lines): From 0c1f164a08eaf60d9c4642422f80b7f2c92f4c02 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 22 Jul 2016 15:13:08 -0400 Subject: [PATCH 469/569] log baud/protocol in the correct places --- obd/elm327.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/elm327.py b/obd/elm327.py index 7bc776bb..19e45b7f 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -171,8 +171,8 @@ def __init__(self, portname, baudrate, protocol): logger.info("Connected Successfully: PORT=%s BAUD=%s PROTOCOL=%s" % ( portname, - self.__protocol.ELM_ID, self.__port.baudrate, + self.__protocol.ELM_ID, )) else: logger.error("Connected to the adapter, but failed to connect to the vehicle") From 70ecdb59b8e7a1f08f24f85f752fd912c3e0a34a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 25 Jul 2016 20:57:42 -0400 Subject: [PATCH 470/569] return early if set_baudrate fails --- obd/elm327.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/obd/elm327.py b/obd/elm327.py index 19e45b7f..f4175eea 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -134,7 +134,8 @@ def __init__(self, portname, baudrate, protocol): # ------------------------ find the ELM's baud ------------------------ if not self.set_baudrate(baudrate): - self.__error("Failed to set baudrate"); + self.__error("Failed to set baudrate") + return # ---------------------------- ATZ (reset) ---------------------------- try: From a22c1616b118af94009d5aff3188742e3260a593 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 25 Jul 2016 21:20:51 -0400 Subject: [PATCH 471/569] added UAS to the control flow diagram --- obd/README.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/obd/README.md b/obd/README.md index 98aa3cc3..1a145d0e 100644 --- a/obd/README.md +++ b/obd/README.md @@ -1,18 +1,15 @@ - -Notes ------ - ``` + API ┌───────────────────────┐ -│ obd.py (API) │ +│ obd.py / async.py │ └───┰───────────────────┘ ┃ ▲ ┃ ┃ -┌───╂───────────────╂───┐ ┌─────────────────┐ -│ ┃ ┗━━━┿━━━━━━┥ │ -│ ┃ OBDCommand.py │ │ decoders.py │ -│ ┃ ┏━━━┿━━━━ ▶│ │ -└───╂───────────────╂───┘ └─────────────────┘ +┌───╂───────────────╂───┐ ┌─────────────────┐ ┌────────────────────┐ +│ ┃ ┗━━━┿━━━━━━┥ │◀ ━━━━━━━┥ │ +│ ┃ OBDCommand.py │ │ decoders.py │ (maybe) │ UnitsAndScaling.py │ +│ ┃ ┏━━━┿━━━━ ▶│ ┝━━━━━━━ ▶│ │ +└───╂───────────────╂───┘ └─────────────────┘ └────────────────────┘ ┃ ┃ ┃ ┃ ┌───╂───────────────╂───┐ ┌─────────────────┐ @@ -25,4 +22,5 @@ Notes ┌───────────────────┸───┐ │ pyserial │ └───────────────────────┘ + Serial Port ``` From d9d7ff82bf242b9b575a565c0d91c7c20bf61ddf Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 25 Jul 2016 21:30:22 -0400 Subject: [PATCH 472/569] added notes on files that aren't in the diagram --- obd/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/obd/README.md b/obd/README.md index 1a145d0e..7788290e 100644 --- a/obd/README.md +++ b/obd/README.md @@ -24,3 +24,9 @@ └───────────────────────┘ Serial Port ``` + +Not pictured: + +- `commands.py` : defines the various OBD commands, and which decoder they use +- `codes.py` : stores tables of standardized values needed by `decoders.py` (mostly check-engine codes) +- `OBDResponse.py` : defines structures/objects returned by the API in response to a query. From 348e5cfac57b416b85998a7e7beed9da86f60f58 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 27 Jul 2016 17:42:30 -0400 Subject: [PATCH 473/569] elevated debug level to warning --- obd/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/__init__.py b/obd/__init__.py index 1a5c9d1b..dc96b281 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -49,7 +49,7 @@ import logging logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) +logger.setLevel(logging.WARNING) console_handler = logging.StreamHandler() # sends output to stderr console_handler.setFormatter(logging.Formatter("[%(name)s] %(message)s")) From b4fcfaa98ca0625e193fd2a7ffbc3de203da1df2 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 27 Jul 2016 17:50:55 -0400 Subject: [PATCH 474/569] silence test_cmd output when scanning PID support --- obd/obd.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/obd/obd.py b/obd/obd.py index e3ad3732..34899651 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -104,7 +104,7 @@ def __load_commands(self): for get in pid_getters: # PID listing commands should sequentialy become supported # Mode 1 PID 0 is assumed to always be supported - if not self.supports(get): + if not self.test_cmd(get, warn=False): continue # when querying, only use the blocking OBD.query() @@ -214,19 +214,21 @@ def supports(self, cmd): return cmd in self.supported_commands - def test_cmd(self, cmd): + def test_cmd(self, cmd, warn=True): """ Returns a boolean for whether a command will be sent without using force=True. """ # test if the command is supported if not self.supports(cmd): - logger.warning("'%s' is not supported" % str(cmd)) + if warn: + logger.warning("'%s' is not supported" % str(cmd)) return False # mode 06 is only implemented for the CAN protocols if cmd.mode == 6 and self.interface.protocol_id() not in ["6", "7", "8", "9"]: - logger.warning("Mode 06 commands are only supported over CAN protocols") + if warn: + logger.warning("Mode 06 commands are only supported over CAN protocols") return False return True From 8ae8f44f8674a91eeee486b337208b3bdb278c2b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 27 Jul 2016 23:32:39 -0400 Subject: [PATCH 475/569] fixed custom command example code --- docs/Custom Commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Custom Commands.md b/docs/Custom Commands.md index cef34172..71fe1cb2 100644 --- a/docs/Custom Commands.md +++ b/docs/Custom Commands.md @@ -16,7 +16,7 @@ Example ------- ```python -from obd import OBDCommand +from obd import OBDCommand, Unit from obd.protocols import ECU from obd.utils import bytes_to_int From c11ed0d503f6e53d62e1a83a711b7d98f9054ba8 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 27 Jul 2016 23:33:28 -0400 Subject: [PATCH 476/569] fixed comment to reflect new decoder format --- obd/decoders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/decoders.py b/obd/decoders.py index ce7b31c8..7ea0a6bd 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -45,7 +45,7 @@ def (): ... - return (, ) + return ''' From 4638a1b509dbc166f0841e497001c7f1210bda3e Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 1 Aug 2016 20:30:53 -0400 Subject: [PATCH 477/569] added utf-8 coding marks so I can use unicode in my comments --- obd/OBDCommand.py | 1 + obd/OBDResponse.py | 1 + obd/UnitsAndScaling.py | 1 + obd/__init__.py | 1 + obd/async.py | 1 + obd/codes.py | 1 + obd/commands.py | 1 + obd/decoders.py | 1 + obd/elm327.py | 1 + obd/obd.py | 1 + obd/protocols/__init__.py | 1 + obd/protocols/protocol.py | 1 + obd/protocols/protocol_can.py | 1 + obd/protocols/protocol_legacy.py | 1 + obd/protocols/protocol_unknown.py | 1 + obd/utils.py | 1 + 16 files changed, 16 insertions(+) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 2a3f2e09..25f78d7a 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ######################################################################## # # diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py index e3c7d550..4d697eaf 100644 --- a/obd/OBDResponse.py +++ b/obd/OBDResponse.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ######################################################################## # # diff --git a/obd/UnitsAndScaling.py b/obd/UnitsAndScaling.py index 4fbe9f1e..0d58de39 100644 --- a/obd/UnitsAndScaling.py +++ b/obd/UnitsAndScaling.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ######################################################################## # # diff --git a/obd/__init__.py b/obd/__init__.py index dc96b281..8103db45 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ A serial module for accessing data from a vehicles OBD-II port diff --git a/obd/async.py b/obd/async.py index ecd43746..335c5872 100644 --- a/obd/async.py +++ b/obd/async.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ######################################################################## # # diff --git a/obd/codes.py b/obd/codes.py index 97202e92..b8fea917 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ######################################################################## # # diff --git a/obd/commands.py b/obd/commands.py index b5735ce2..874a81f0 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ######################################################################## # # diff --git a/obd/decoders.py b/obd/decoders.py index 7ea0a6bd..674fa368 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ######################################################################## # # diff --git a/obd/elm327.py b/obd/elm327.py index f4175eea..c469975b 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ######################################################################## # # diff --git a/obd/obd.py b/obd/obd.py index 34899651..52140f6e 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ######################################################################## # # diff --git a/obd/protocols/__init__.py b/obd/protocols/__init__.py index 9860372d..bfec7dee 100644 --- a/obd/protocols/__init__.py +++ b/obd/protocols/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ######################################################################## # # diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index c8e7ab71..40619f89 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ######################################################################## # # diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index e2381f79..9a76e0af 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ######################################################################## # # diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 1e24eeea..573b6c5c 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ######################################################################## # # diff --git a/obd/protocols/protocol_unknown.py b/obd/protocols/protocol_unknown.py index 69fac01a..0a94fadb 100644 --- a/obd/protocols/protocol_unknown.py +++ b/obd/protocols/protocol_unknown.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ######################################################################## # # diff --git a/obd/utils.py b/obd/utils.py index b1061693..a5ee9351 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- ######################################################################## # # From 2e2116e9529b1dda2e523d06daafbd13e11c8f7f Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 1 Aug 2016 20:58:06 -0400 Subject: [PATCH 478/569] fixed link to command tables --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bf54aa1c..463f35c7 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Available at [python-obd.readthedocs.org](http://python-obd.readthedocs.org/en/l Commands -------- -Here are a handful of the supported commands (sensors). For a full list, see [the docs](http://python-obd.readthedocs.org/en/latest/Commands/#mode-01) +Here are a handful of the supported commands (sensors). For a full list, see [the docs](http://python-obd.readthedocs.io/en/latest/Command%20Tables/) *note: support for these commands will vary from car to car* From fd1a26ee6f01bd956dcff94dae62d6219ee01cc8 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 1 Aug 2016 21:03:18 -0400 Subject: [PATCH 479/569] bumped to v0.6.1 --- obd/__version__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/__version__.py b/obd/__version__.py index 1f199f19..7a2d0cd1 100644 --- a/obd/__version__.py +++ b/obd/__version__.py @@ -1,2 +1,2 @@ -__version__ = '0.6.0' +__version__ = '0.6.1' diff --git a/setup.py b/setup.py index 21bd121b..51e13966 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="obd", - version="0.6.0", + version="0.6.1", description=("Serial module for handling live sensor data from a vehicle's OBD-II port"), classifiers=[ "Operating System :: POSIX :: Linux", From 108726be836a84eeb07c4f447558e6a0107705ba Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Mon, 8 Aug 2016 19:32:00 -0400 Subject: [PATCH 480/569] pegged dependency versions --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 51e13966..60835a1b 100644 --- a/setup.py +++ b/setup.py @@ -25,5 +25,5 @@ packages=find_packages(), include_package_data=True, zip_safe=False, - install_requires=["pyserial", "pint"], + install_requires=["pyserial==3.*", "pint==0.7.*"], ) From 4336e20e3155b367d7450b180ceae5fdedd439e8 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 19 Aug 2016 14:52:48 -0400 Subject: [PATCH 481/569] fixed arg names for fuel_type and obd_compliance, added tests --- obd/decoders.py | 12 ++++++++---- tests/test_decoders.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 674fa368..2aa70642 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -327,26 +327,30 @@ def air_status(messages): return status -def obd_compliance(_hex): +def obd_compliance(messages): d = messages[0].data i = d[0] - v = "Error: Unknown OBD compliance response" + v = None if i < len(OBD_COMPLIANCE): v = OBD_COMPLIANCE[i] + else: + logger.debug("Invalid response for OBD compliance (no table entry)") return v -def fuel_type(_hex): +def fuel_type(messages): d = messages[0].data i = d[0] # todo, support second fuel system - v = "Error: Unknown fuel type response" + v = None if i < len(FUEL_TYPES): v = FUEL_TYPES[i] + else: + logger.debug("Invalid response for fuel type (no table entry)") return v diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 5ef0f238..10d13500 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -129,6 +129,16 @@ def test_air_status(): assert d.air_status(m("08")) == "Pump commanded on for diagnostics" assert d.air_status(m("03")) == None +def test_fuel_type(): + assert d.fuel_type(m("00")) == "Not available" + assert d.fuel_type(m("17")) == "Bifuel running diesel" + assert d.fuel_type(m("18")) == None + +def test_obd_compliance(): + assert d.obd_compliance(m("00")) == "Undefined" + assert d.obd_compliance(m("21")) == "Heavy Duty Euro OBD Stage VI (HD EOBD-IV)" + assert d.obd_compliance(m("22")) == None + def test_o2_sensors(): assert d.o2_sensors(m("00")) == ((),(False, False, False, False), (False, False, False, False)) assert d.o2_sensors(m("01")) == ((),(False, False, False, False), (False, False, False, True)) From 4d1baf92bbf2d40b75169483187c89e7868fa431 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 25 Aug 2016 22:05:17 -0400 Subject: [PATCH 482/569] OBDCommands use bytestrings now --- tests/test_OBDCommand.py | 22 +++++++++++----------- tests/test_commands.py | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index d97c02ad..87ea3277 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -10,10 +10,10 @@ def test_constructor(): # default constructor # name description cmd bytes decoder ECU - cmd = OBDCommand("Test", "example OBD command", "0123", 2, noop, ECU.ENGINE) + cmd = OBDCommand("Test", "example OBD command", b"0123", 2, noop, ECU.ENGINE) assert cmd.name == "Test" assert cmd.desc == "example OBD command" - assert cmd.command == "0123" + assert cmd.command == b"0123" assert cmd.bytes == 2 assert cmd.decode == noop assert cmd.ecu == ECU.ENGINE @@ -24,14 +24,14 @@ def test_constructor(): # a case where "fast", and "supported" were set explicitly # name description cmd bytes decoder ECU fast - cmd = OBDCommand("Test 2", "example OBD command", "0123", 2, noop, ECU.ENGINE, True) + cmd = OBDCommand("Test 2", "example OBD command", b"0123", 2, noop, ECU.ENGINE, True) assert cmd.fast == True def test_clone(): # name description mode cmd bytes decoder - cmd = OBDCommand("", "", "0123", 2, noop, ECU.ENGINE) + cmd = OBDCommand("", "", b"0123", 2, noop, ECU.ENGINE) other = cmd.clone() assert cmd.name == other.name @@ -51,34 +51,34 @@ def test_call(): print(messages[0].data) # valid response size - cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) + cmd = OBDCommand("", "", b"0123", 4, noop, ECU.ENGINE) r = cmd(messages) assert r.value == b'\xBE\x1F\xB8\x11' # response too short (pad) - cmd = OBDCommand("", "", "0123", 5, noop, ECU.ENGINE) + cmd = OBDCommand("", "", b"0123", 5, noop, ECU.ENGINE) r = cmd(messages) assert r.value == b'\xBE\x1F\xB8\x11\x00' # response too long (clip) - cmd = OBDCommand("", "", "0123", 3, noop, ECU.ENGINE) + cmd = OBDCommand("", "", b"0123", 3, noop, ECU.ENGINE) r = cmd(messages) assert r.value == b'\xBE\x1F\xB8' def test_get_mode(): - cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) + cmd = OBDCommand("", "", b"0123", 4, noop, ECU.ENGINE) assert cmd.mode == 0x01 - cmd = OBDCommand("", "", "", "23", 4, noop, ECU.ENGINE) + cmd = OBDCommand("", "", b"", 4, noop, ECU.ENGINE) assert cmd.mode == 0 def test_pid(): - cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE) + cmd = OBDCommand("", "", b"0123", 4, noop, ECU.ENGINE) assert cmd.pid == 0x23 - cmd = OBDCommand("", "", "01", 4, noop, ECU.ENGINE) + cmd = OBDCommand("", "", b"01", 4, noop, ECU.ENGINE) assert cmd.pid == 0 diff --git a/tests/test_commands.py b/tests/test_commands.py index 1b257d55..eb80837a 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -10,7 +10,7 @@ def test_list_integrity(): if cmd is None: continue # this command is reserved - assert cmd.command != "", "The Command's command string must not be null" + assert cmd.command != b"", "The Command's command string must not be null" # make sure the command tables are in mode & PID order assert mode == cmd.mode, "Command is in the wrong mode list: %s" % cmd.name From dcd6b7835ffb7cc4f1138653f6c6485da63bcf3c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 25 Aug 2016 22:24:45 -0400 Subject: [PATCH 483/569] check commands for hex content before reporting integer mode/PID --- obd/OBDCommand.py | 6 ++++-- tests/test_OBDCommand.py | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 25f78d7a..663a6a07 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -67,14 +67,16 @@ def clone(self): @property def mode(self): - if len(self.command) >= 2: + if len(self.command) >= 2 and \ + isHex(self.command.decode()): return int(self.command[:2], 16) else: return 0 @property def pid(self): - if len(self.command) > 2: + if len(self.command) > 2 and \ + isHex(self.command.decode()): return int(self.command[2:], 16) else: return 0 diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index 87ea3277..e59f61fc 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -74,6 +74,8 @@ def test_get_mode(): cmd = OBDCommand("", "", b"", 4, noop, ECU.ENGINE) assert cmd.mode == 0 + cmd = OBDCommand("", "", b"totally not hex", 4, noop, ECU.ENGINE) + assert cmd.mode == 0 def test_pid(): @@ -82,3 +84,6 @@ def test_pid(): cmd = OBDCommand("", "", b"01", 4, noop, ECU.ENGINE) assert cmd.pid == 0 + + cmd = OBDCommand("", "", b"totally not hex", 4, noop, ECU.ENGINE) + assert cmd.mode == 0 From b94dfadca26fc36ff360a34500c236495dc9154d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 25 Aug 2016 22:45:19 -0400 Subject: [PATCH 484/569] allow OBDCommands to return mode/PID of None --- obd/OBDCommand.py | 4 ++-- tests/test_OBDCommand.py | 8 ++++---- tests/test_commands.py | 25 ++++++++++++++++++------- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 663a6a07..18762a53 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -71,7 +71,7 @@ def mode(self): isHex(self.command.decode()): return int(self.command[:2], 16) else: - return 0 + return None @property def pid(self): @@ -79,7 +79,7 @@ def pid(self): isHex(self.command.decode()): return int(self.command[2:], 16) else: - return 0 + return None def __call__(self, messages): diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index e59f61fc..f8727c07 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -72,10 +72,10 @@ def test_get_mode(): assert cmd.mode == 0x01 cmd = OBDCommand("", "", b"", 4, noop, ECU.ENGINE) - assert cmd.mode == 0 + assert cmd.mode == None cmd = OBDCommand("", "", b"totally not hex", 4, noop, ECU.ENGINE) - assert cmd.mode == 0 + assert cmd.mode == None def test_pid(): @@ -83,7 +83,7 @@ def test_pid(): assert cmd.pid == 0x23 cmd = OBDCommand("", "", b"01", 4, noop, ECU.ENGINE) - assert cmd.pid == 0 + assert cmd.pid == None cmd = OBDCommand("", "", b"totally not hex", 4, noop, ECU.ENGINE) - assert cmd.mode == 0 + assert cmd.mode == None diff --git a/tests/test_commands.py b/tests/test_commands.py index eb80837a..3d6c033e 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -14,7 +14,12 @@ def test_list_integrity(): # make sure the command tables are in mode & PID order assert mode == cmd.mode, "Command is in the wrong mode list: %s" % cmd.name - assert pid == cmd.pid, "The index in the list must also be the PID: %s" % cmd.name + + if len(cmds) > 1: + assert pid == cmd.pid, "The index in the list must also be the PID: %s" % cmd.name + else: + # lone commands in a mode are allowed to have no PID + assert (pid == cmd.pid) or (cmd.pid is None) # make sure all the fields are set assert cmd.name != "", "Command names must not be null" @@ -50,9 +55,12 @@ def test_getitem(): continue # this command is reserved # by [mode][pid] - mode = cmd.mode - pid = cmd.pid - assert cmd == obd.commands[mode][pid], "mode %d, PID %d could not be accessed through __getitem__" % (mode, pid) + if (cmd.pid is None) and (len(cmds) == 1): + # lone commands in a mode have no PID, and report a pid + # value of None, but can still be accessed by PID 0 + assert cmd == obd.commands[cmd.mode][0], "lone command in mode %d could not be accessed through __getitem__" % mode + else: + assert cmd == obd.commands[cmd.mode][cmd.pid], "mode %d, PID %d could not be accessed through __getitem__" % (mode, pid) # by [name] assert cmd == obd.commands[cmd.name], "command name %s could not be accessed through __getitem__" % (cmd.name) @@ -70,9 +78,12 @@ def test_contains(): assert obd.commands.has_command(cmd) # by (mode, pid) - mode = cmd.mode - pid = cmd.pid - assert obd.commands.has_pid(mode, pid) + if cmd.pid is None: + # lone commands in a mode can have no PID, and report None + # but these commands can still be looked up by (mode, pid=0) + assert obd.commands.has_pid(cmd.mode, 0) + else: + assert obd.commands.has_pid(cmd.mode, cmd.pid) # by (name) assert obd.commands.has_name(cmd.name) From 6dd6b846beb3e7ba5c3e9ce43c5d362a5af5011d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Thu, 25 Aug 2016 23:08:55 -0400 Subject: [PATCH 485/569] added travis --- .travis.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..ad22f32f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: python + +python: + - 2.7 + - 3.2 + - 3.3 + - 3.4 + - 3.5 + +script: + - python setup.py install + - pip install pytest + - py.test From 36784bffc2ca50bad7b0f7941189eda8a9865467 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Fri, 26 Aug 2016 18:05:39 -0400 Subject: [PATCH 486/569] response limit is frame-based, not ecu-based --- obd/obd.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/obd/obd.py b/obd/obd.py index 52140f6e..221725b1 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -51,8 +51,9 @@ class OBD(object): def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True): self.interface = None self.supported_commands = set(commands.base_commands()) - self.fast = fast - self.__last_command = "" # used for running the previous command with a CR + self.fast = fast # global switch for disabling optimizations + self.__last_command = b"" # used for running the previous command with a CR + self.__frame_counts = {} # keeps track of the number of return frames for each command logger.info("======================= python-OBD (v%s) =======================" % __version__) self.__connect(portstr, baudrate, protocol) # initialize by connecting and loading sensors @@ -255,9 +256,16 @@ def query(self, cmd, force=False): messages = self.interface.send_and_parse(cmd_string) # if we're sending a new command, note it + # first check that the current command WASN'T sent as an empty CR + # (CR is added by the ELM327 class) if cmd_string: self.__last_command = cmd_string + # if we don't already know how many frames this command returns, + # log it, so we can specify it next time + if cmd not in self.__frame_counts: + self.__frame_counts[cmd] = sum([len(m.frames) for m in messages]) + if not messages: logger.info("No valid OBD Messages returned") return OBDResponse() @@ -269,11 +277,14 @@ def __build_command_string(self, cmd): """ assembles the appropriate command string """ cmd_string = cmd.command - # only wait for as many ECUs as we've seen - if self.fast and cmd.fast: - cmd_string += str(len(self.interface.ecus())).encode() + # if we know the number of frames that this command returns, + # only wait for exactly that number. This avoids some harsh + # timeouts from the ELM, thus speeding up queries. + if self.fast and cmd.fast and (cmd in self.__frame_counts): + cmd_string += str(self.__frame_counts[cmd]).encode() - # if we sent this last time, just send + # if we sent this last time, just send a CR + # (CR is added by the ELM327 class) if self.fast and (cmd_string == self.__last_command): cmd_string = b"" From f664264d9aed87d48ce9803844e1fbb07fd1d46a Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 3 Sep 2016 20:54:34 -0400 Subject: [PATCH 487/569] clarity in order --- obd/OBDCommand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 18762a53..5b5b9735 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -85,7 +85,7 @@ def pid(self): def __call__(self, messages): # filter for applicable messages (from the right ECU(s)) - for_us = lambda m: self.ecu & m.ecu > 0 + for_us = lambda m: (self.ecu & m.ecu) > 0 messages = list(filter(for_us, messages)) # guarantee data size for the decoder From 82bc5857c38796e57885ae785a4dea57e5818429 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sat, 10 Sep 2016 23:45:38 -0400 Subject: [PATCH 488/569] CAN protocol no longer removes mode/PID bytes --- obd/commands.py | 192 +++++++++++++++++----------------- obd/decoders.py | 59 ++++++----- obd/protocols/protocol_can.py | 8 +- 3 files changed, 130 insertions(+), 129 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 874a81f0..0f511009 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -51,106 +51,106 @@ __mode1__ = [ # name description cmd bytes decoder ECU fast - OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , b"0100", 4, pid, ECU.ENGINE, True), - OBDCommand("STATUS" , "Status since DTCs cleared" , b"0101", 4, status, ECU.ENGINE, True), - OBDCommand("FREEZE_DTC" , "DTC that triggered the freeze frame" , b"0102", 2, single_dtc, ECU.ENGINE, True), - OBDCommand("FUEL_STATUS" , "Fuel System Status" , b"0103", 2, fuel_status, ECU.ENGINE, True), - OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , b"0104", 1, percent, ECU.ENGINE, True), - OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , b"0105", 1, temp, ECU.ENGINE, True), - OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , b"0106", 1, percent_centered, ECU.ENGINE, True), - OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , b"0107", 1, percent_centered, ECU.ENGINE, True), - OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , b"0108", 1, percent_centered, ECU.ENGINE, True), - OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , b"0109", 1, percent_centered, ECU.ENGINE, True), - OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , b"010A", 1, fuel_pressure, ECU.ENGINE, True), - OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , b"010B", 1, pressure, ECU.ENGINE, True), - OBDCommand("RPM" , "Engine RPM" , b"010C", 2, uas(0x07), ECU.ENGINE, True), - OBDCommand("SPEED" , "Vehicle Speed" , b"010D", 1, uas(0x09), ECU.ENGINE, True), - OBDCommand("TIMING_ADVANCE" , "Timing Advance" , b"010E", 1, timing_advance, ECU.ENGINE, True), - OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , b"010F", 1, temp, ECU.ENGINE, True), - OBDCommand("MAF" , "Air Flow Rate (MAF)" , b"0110", 2, uas(0x27), ECU.ENGINE, True), - OBDCommand("THROTTLE_POS" , "Throttle Position" , b"0111", 1, percent, ECU.ENGINE, True), - OBDCommand("AIR_STATUS" , "Secondary Air Status" , b"0112", 1, air_status, ECU.ENGINE, True), - OBDCommand("O2_SENSORS" , "O2 Sensors Present" , b"0113", 1, o2_sensors, ECU.ENGINE, True), - OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , b"0114", 2, sensor_voltage, ECU.ENGINE, True), - OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , b"0115", 2, sensor_voltage, ECU.ENGINE, True), - OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , b"0116", 2, sensor_voltage, ECU.ENGINE, True), - OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , b"0117", 2, sensor_voltage, ECU.ENGINE, True), - OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , b"0118", 2, sensor_voltage, ECU.ENGINE, True), - OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , b"0119", 2, sensor_voltage, ECU.ENGINE, True), - OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , b"011A", 2, sensor_voltage, ECU.ENGINE, True), - OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , b"011B", 2, sensor_voltage, ECU.ENGINE, True), - OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , b"011C", 1, obd_compliance, ECU.ENGINE, True), - OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , b"011D", 1, o2_sensors_alt, ECU.ENGINE, True), - OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status (power take off)" , b"011E", 1, aux_input_status, ECU.ENGINE, True), - OBDCommand("RUN_TIME" , "Engine Run Time" , b"011F", 2, uas(0x12), ECU.ENGINE, True), + OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , b"0100", 6, pid, ECU.ENGINE, True), + OBDCommand("STATUS" , "Status since DTCs cleared" , b"0101", 6, status, ECU.ENGINE, True), + OBDCommand("FREEZE_DTC" , "DTC that triggered the freeze frame" , b"0102", 4, single_dtc, ECU.ENGINE, True), + OBDCommand("FUEL_STATUS" , "Fuel System Status" , b"0103", 4, fuel_status, ECU.ENGINE, True), + OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , b"0104", 3, percent, ECU.ENGINE, True), + OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , b"0105", 3, temp, ECU.ENGINE, True), + OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , b"0106", 3, percent_centered, ECU.ENGINE, True), + OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , b"0107", 3, percent_centered, ECU.ENGINE, True), + OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , b"0108", 3, percent_centered, ECU.ENGINE, True), + OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , b"0109", 3, percent_centered, ECU.ENGINE, True), + OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , b"010A", 3, fuel_pressure, ECU.ENGINE, True), + OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , b"010B", 3, pressure, ECU.ENGINE, True), + OBDCommand("RPM" , "Engine RPM" , b"010C", 4, uas(0x07), ECU.ENGINE, True), + OBDCommand("SPEED" , "Vehicle Speed" , b"010D", 3, uas(0x09), ECU.ENGINE, True), + OBDCommand("TIMING_ADVANCE" , "Timing Advance" , b"010E", 3, timing_advance, ECU.ENGINE, True), + OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , b"010F", 3, temp, ECU.ENGINE, True), + OBDCommand("MAF" , "Air Flow Rate (MAF)" , b"0110", 4, uas(0x27), ECU.ENGINE, True), + OBDCommand("THROTTLE_POS" , "Throttle Position" , b"0111", 3, percent, ECU.ENGINE, True), + OBDCommand("AIR_STATUS" , "Secondary Air Status" , b"0112", 3, air_status, ECU.ENGINE, True), + OBDCommand("O2_SENSORS" , "O2 Sensors Present" , b"0113", 3, o2_sensors, ECU.ENGINE, True), + OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , b"0114", 4, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , b"0115", 4, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , b"0116", 4, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , b"0117", 4, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , b"0118", 4, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , b"0119", 4, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , b"011A", 4, sensor_voltage, ECU.ENGINE, True), + OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , b"011B", 4, sensor_voltage, ECU.ENGINE, True), + OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , b"011C", 3, obd_compliance, ECU.ENGINE, True), + OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , b"011D", 3, o2_sensors_alt, ECU.ENGINE, True), + OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status (power take off)" , b"011E", 3, aux_input_status, ECU.ENGINE, True), + OBDCommand("RUN_TIME" , "Engine Run Time" , b"011F", 4, uas(0x12), ECU.ENGINE, True), # name description cmd bytes decoder ECU fast - OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , b"0120", 4, pid, ECU.ENGINE, True), - OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , b"0121", 2, uas(0x25), ECU.ENGINE, True), - OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , b"0122", 2, uas(0x19), ECU.ENGINE, True), - OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , b"0123", 2, uas(0x1B), ECU.ENGINE, True), - OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , b"0124", 4, sensor_voltage_big, ECU.ENGINE, True), - OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , b"0125", 4, sensor_voltage_big, ECU.ENGINE, True), - OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , b"0126", 4, sensor_voltage_big, ECU.ENGINE, True), - OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , b"0127", 4, sensor_voltage_big, ECU.ENGINE, True), - OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , b"0128", 4, sensor_voltage_big, ECU.ENGINE, True), - OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , b"0129", 4, sensor_voltage_big, ECU.ENGINE, True), - OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , b"012A", 4, sensor_voltage_big, ECU.ENGINE, True), - OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , b"012B", 4, sensor_voltage_big, ECU.ENGINE, True), - OBDCommand("COMMANDED_EGR" , "Commanded EGR" , b"012C", 1, percent, ECU.ENGINE, True), - OBDCommand("EGR_ERROR" , "EGR Error" , b"012D", 1, percent_centered, ECU.ENGINE, True), - OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , b"012E", 1, percent, ECU.ENGINE, True), - OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , b"012F", 1, percent, ECU.ENGINE, True), - OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , b"0130", 1, uas(0x01), ECU.ENGINE, True), - OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , b"0131", 2, uas(0x25), ECU.ENGINE, True), - OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , b"0132", 2, evap_pressure, ECU.ENGINE, True), - OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , b"0133", 1, pressure, ECU.ENGINE, True), - OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , b"0134", 4, current_centered, ECU.ENGINE, True), - OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , b"0135", 4, current_centered, ECU.ENGINE, True), - OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , b"0136", 4, current_centered, ECU.ENGINE, True), - OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , b"0137", 4, current_centered, ECU.ENGINE, True), - OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , b"0138", 4, current_centered, ECU.ENGINE, True), - OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , b"0139", 4, current_centered, ECU.ENGINE, True), - OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , b"013A", 4, current_centered, ECU.ENGINE, True), - OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , b"013B", 4, current_centered, ECU.ENGINE, True), - OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , b"013C", 2, uas(0x16), ECU.ENGINE, True), - OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , b"013D", 2, uas(0x16), ECU.ENGINE, True), - OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , b"013E", 2, uas(0x16), ECU.ENGINE, True), - OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , b"013F", 2, uas(0x16), ECU.ENGINE, True), + OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , b"0120", 6, pid, ECU.ENGINE, True), + OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , b"0121", 4, uas(0x25), ECU.ENGINE, True), + OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , b"0122", 4, uas(0x19), ECU.ENGINE, True), + OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , b"0123", 4, uas(0x1B), ECU.ENGINE, True), + OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , b"0124", 6, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , b"0125", 6, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , b"0126", 6, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , b"0127", 6, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , b"0128", 6, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , b"0129", 6, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , b"012A", 6, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , b"012B", 6, sensor_voltage_big, ECU.ENGINE, True), + OBDCommand("COMMANDED_EGR" , "Commanded EGR" , b"012C", 3, percent, ECU.ENGINE, True), + OBDCommand("EGR_ERROR" , "EGR Error" , b"012D", 3, percent_centered, ECU.ENGINE, True), + OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , b"012E", 3, percent, ECU.ENGINE, True), + OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , b"012F", 3, percent, ECU.ENGINE, True), + OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , b"0130", 3, uas(0x01), ECU.ENGINE, True), + OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , b"0131", 4, uas(0x25), ECU.ENGINE, True), + OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , b"0132", 4, evap_pressure, ECU.ENGINE, True), + OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , b"0133", 3, pressure, ECU.ENGINE, True), + OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , b"0134", 6, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , b"0135", 6, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , b"0136", 6, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , b"0137", 6, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , b"0138", 6, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , b"0139", 6, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , b"013A", 6, current_centered, ECU.ENGINE, True), + OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , b"013B", 6, current_centered, ECU.ENGINE, True), + OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , b"013C", 4, uas(0x16), ECU.ENGINE, True), + OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , b"013D", 4, uas(0x16), ECU.ENGINE, True), + OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , b"013E", 4, uas(0x16), ECU.ENGINE, True), + OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , b"013F", 4, uas(0x16), ECU.ENGINE, True), # name description cmd bytes decoder ECU fast - OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , b"0140", 4, pid, ECU.ENGINE, True), - OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , b"0141", 4, status, ECU.ENGINE, True), - OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , b"0142", 2, uas(0x0B), ECU.ENGINE, True), - OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , b"0143", 2, absolute_load, ECU.ENGINE, True), - OBDCommand("COMMANDED_EQUIV_RATIO" , "Commanded equivalence ratio" , b"0144", 2, uas(0x1E), ECU.ENGINE, True), - OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , b"0145", 1, percent, ECU.ENGINE, True), - OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , b"0146", 1, temp, ECU.ENGINE, True), - OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , b"0147", 1, percent, ECU.ENGINE, True), - OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , b"0148", 1, percent, ECU.ENGINE, True), - OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , b"0149", 1, percent, ECU.ENGINE, True), - OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , b"014A", 1, percent, ECU.ENGINE, True), - OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , b"014B", 1, percent, ECU.ENGINE, True), - OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , b"014C", 1, percent, ECU.ENGINE, True), - OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , b"014D", 2, uas(0x34), ECU.ENGINE, True), - OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , b"014E", 2, uas(0x34), ECU.ENGINE, True), - OBDCommand("MAX_VALUES" , "Various Max values" , b"014F", 4, drop, ECU.ENGINE, True), # todo: decode this - OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , b"0150", 4, max_maf, ECU.ENGINE, True), - OBDCommand("FUEL_TYPE" , "Fuel Type" , b"0151", 1, fuel_type, ECU.ENGINE, True), - OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , b"0152", 1, percent, ECU.ENGINE, True), - OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , b"0153", 2, abs_evap_pressure, ECU.ENGINE, True), - OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , b"0154", 2, evap_pressure_alt, ECU.ENGINE, True), - OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , b"0155", 2, percent_centered, ECU.ENGINE, True), # todo: decode seconds value for banks 3 and 4 - OBDCommand("LONG_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , b"0156", 2, percent_centered, ECU.ENGINE, True), - OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , b"0157", 2, percent_centered, ECU.ENGINE, True), - OBDCommand("LONG_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , b"0158", 2, percent_centered, ECU.ENGINE, True), - OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , b"0159", 2, uas(0x1B), ECU.ENGINE, True), - OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , b"015A", 1, percent, ECU.ENGINE, True), - OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , b"015B", 1, percent, ECU.ENGINE, True), - OBDCommand("OIL_TEMP" , "Engine oil temperature" , b"015C", 1, temp, ECU.ENGINE, True), - OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , b"015D", 2, inject_timing, ECU.ENGINE, True), - OBDCommand("FUEL_RATE" , "Engine fuel rate" , b"015E", 2, fuel_rate, ECU.ENGINE, True), - OBDCommand("EMISSION_REQ" , "Designed emission requirements" , b"015F", 1, drop, ECU.ENGINE, True), + OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , b"0140", 6, pid, ECU.ENGINE, True), + OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , b"0141", 6, status, ECU.ENGINE, True), + OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , b"0142", 4, uas(0x0B), ECU.ENGINE, True), + OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , b"0143", 4, absolute_load, ECU.ENGINE, True), + OBDCommand("COMMANDED_EQUIV_RATIO" , "Commanded equivalence ratio" , b"0144", 4, uas(0x1E), ECU.ENGINE, True), + OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , b"0145", 3, percent, ECU.ENGINE, True), + OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , b"0146", 3, temp, ECU.ENGINE, True), + OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , b"0147", 3, percent, ECU.ENGINE, True), + OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , b"0148", 3, percent, ECU.ENGINE, True), + OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , b"0149", 3, percent, ECU.ENGINE, True), + OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , b"014A", 3, percent, ECU.ENGINE, True), + OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , b"014B", 3, percent, ECU.ENGINE, True), + OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , b"014C", 3, percent, ECU.ENGINE, True), + OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , b"014D", 4, uas(0x34), ECU.ENGINE, True), + OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , b"014E", 4, uas(0x34), ECU.ENGINE, True), + OBDCommand("MAX_VALUES" , "Various Max values" , b"014F", 6, drop, ECU.ENGINE, True), # todo: decode this + OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , b"0150", 6, max_maf, ECU.ENGINE, True), + OBDCommand("FUEL_TYPE" , "Fuel Type" , b"0151", 3, fuel_type, ECU.ENGINE, True), + OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , b"0152", 3, percent, ECU.ENGINE, True), + OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , b"0153", 4, abs_evap_pressure, ECU.ENGINE, True), + OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , b"0154", 4, evap_pressure_alt, ECU.ENGINE, True), + OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , b"0155", 4, percent_centered, ECU.ENGINE, True), # todo: decode seconds value for banks 3 and 4 + OBDCommand("LONG_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , b"0156", 4, percent_centered, ECU.ENGINE, True), + OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , b"0157", 4, percent_centered, ECU.ENGINE, True), + OBDCommand("LONG_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , b"0158", 4, percent_centered, ECU.ENGINE, True), + OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , b"0159", 4, uas(0x1B), ECU.ENGINE, True), + OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , b"015A", 3, percent, ECU.ENGINE, True), + OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , b"015B", 3, percent, ECU.ENGINE, True), + OBDCommand("OIL_TEMP" , "Engine oil temperature" , b"015C", 3, temp, ECU.ENGINE, True), + OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , b"015D", 4, inject_timing, ECU.ENGINE, True), + OBDCommand("FUEL_RATE" , "Engine fuel rate" , b"015E", 4, fuel_rate, ECU.ENGINE, True), + OBDCommand("EMISSION_REQ" , "Designed emission requirements" , b"015F", 3, drop, ECU.ENGINE, True), ] diff --git a/obd/decoders.py b/obd/decoders.py index 2aa70642..99681b05 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -64,7 +64,7 @@ def noop(messages): # hex in, bitstring out def pid(messages): - d = messages[0].data + d = messages[0].data[2:] return bitarray(d) # returns the raw strings from the ELM @@ -83,7 +83,7 @@ def uas(id): return functools.partial(decode_uas, id=id) def decode_uas(messages, id): - d = messages[0].data + d = messages[0].data[2:] # chop off mode and PID bytes return UAS_IDS[id](d) @@ -94,62 +94,62 @@ def decode_uas(messages, id): # 0 to 100 % def percent(messages): - d = messages[0].data + d = messages[0].data[2:] v = d[0] v = v * 100.0 / 255.0 return v * Unit.percent # -100 to 100 % def percent_centered(messages): - d = messages[0].data + d = messages[0].data[2:] v = d[0] v = (v - 128) * 100.0 / 128.0 return v * Unit.percent # -40 to 215 C def temp(messages): - d = messages[0].data + d = messages[0].data[2:] v = bytes_to_int(d) v = v - 40 return Unit.Quantity(v, Unit.celsius) # non-multiplicative unit # -128 to 128 mA def current_centered(messages): - d = messages[0].data + d = messages[0].data[2:] v = bytes_to_int(d[2:4]) v = (v / 256.0) - 128 return v * Unit.milliampere # 0 to 1.275 volts def sensor_voltage(messages): - d = messages[0].data + d = messages[0].data[2:] v = d[0] / 200.0 return v * Unit.volt # 0 to 8 volts def sensor_voltage_big(messages): - d = messages[0].data + d = messages[0].data[2:] v = bytes_to_int(d[2:4]) v = (v * 8.0) / 65535 return v * Unit.volt # 0 to 765 kPa def fuel_pressure(messages): - d = messages[0].data + d = messages[0].data[2:] v = d[0] v = v * 3 return v * Unit.kilopascal # 0 to 255 kPa def pressure(messages): - d = messages[0].data + d = messages[0].data[2:] v = d[0] return v * Unit.kilopascal # -8192 to 8192 Pa def evap_pressure(messages): # decode the twos complement - d = messages[0].data + d = messages[0].data[2:] a = twos_comp(d[0], 8) b = twos_comp(d[1], 8) v = ((a * 256.0) + b) / 4.0 @@ -157,49 +157,49 @@ def evap_pressure(messages): # 0 to 327.675 kPa def abs_evap_pressure(messages): - d = messages[0].data + d = messages[0].data[2:] v = bytes_to_int(d) v = v / 200.0 return v * Unit.kilopascal # -32767 to 32768 Pa def evap_pressure_alt(messages): - d = messages[0].data + d = messages[0].data[2:] v = bytes_to_int(d) v = v - 32767 return v * Unit.pascal # -64 to 63.5 degrees def timing_advance(messages): - d = messages[0].data + d = messages[0].data[2:] v = d[0] v = (v - 128) / 2.0 return v * Unit.degree # -210 to 301 degrees def inject_timing(messages): - d = messages[0].data + d = messages[0].data[2:] v = bytes_to_int(d) v = (v - 26880) / 128.0 return v * Unit.degree # 0 to 2550 grams/sec def max_maf(messages): - d = messages[0].data + d = messages[0].data[2:] v = d[0] v = v * 10 return v * Unit.gps # 0 to 3212 Liters/hour def fuel_rate(messages): - d = messages[0].data + d = messages[0].data[2:] v = bytes_to_int(d) v = v * 0.05 return v * Unit.liters_per_hour # special bit encoding for PID 13 def o2_sensors(messages): - d = messages[0].data + d = messages[0].data[2:] bits = bitarray(d) return ( (), # bank 0 is invalid @@ -208,12 +208,12 @@ def o2_sensors(messages): ) def aux_input_status(messages): - d = messages[0].data + d = messages[0].data[2:] return ((d[0] >> 7) & 1) == 1 # first bit indicate PTO status # special bit encoding for PID 1D def o2_sensors_alt(messages): - d = messages[0].data + d = messages[0].data[2:] bits = bitarray(d) return ( (), # bank 0 is invalid @@ -225,7 +225,7 @@ def o2_sensors_alt(messages): # 0 to 25700 % def absolute_load(messages): - d = messages[0].data + d = messages[0].data[2:] v = bytes_to_int(d) v *= 100.0 / 255.0 return v * Unit.percent @@ -250,7 +250,7 @@ def elm_voltage(messages): def status(messages): - d = messages[0].data + d = messages[0].data[2:] bits = bitarray(d) # ┌Components not ready @@ -292,7 +292,7 @@ def status(messages): def fuel_status(messages): - d = messages[0].data + d = messages[0].data[2:] bits = bitarray(d) status_1 = "" @@ -315,7 +315,7 @@ def fuel_status(messages): def air_status(messages): - d = messages[0].data + d = messages[0].data[2:] bits = bitarray(d) status = None @@ -328,7 +328,7 @@ def air_status(messages): def obd_compliance(messages): - d = messages[0].data + d = messages[0].data[2:] i = d[0] v = None @@ -342,7 +342,7 @@ def obd_compliance(messages): def fuel_type(messages): - d = messages[0].data + d = messages[0].data[2:] i = d[0] # todo, support second fuel system v = None @@ -379,7 +379,7 @@ def parse_dtc(_bytes): def single_dtc(messages): """ parses a single DTC from a message """ - d = messages[0].data + d = messages[0].data[2:] return parse_dtc(d) @@ -433,7 +433,10 @@ def parse_monitor_test(d, mon): def monitor(messages): - d = messages[0].data + d = messages[0].data[1:] # only dispose of the mode byte. Leave the MID + # even though we never use the MID byte, it may + # show up multiple times. Thus, keeping it make + # for easier parsing. mon = Monitor() # test that we got the right number of bytes diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 9a76e0af..636be225 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -265,9 +265,7 @@ def parse_message(self, message): # chop to the correct size (as specified in the first frame) message.data = message.data[:ff[0].data_len] - - # TODO: this is an ugly solution, maybe move mode/pid byte ignoring to the decoders? - + """ # chop off the Mode/PID bytes based on the mode number mode = message.data[0] if mode == 0x43: @@ -284,7 +282,7 @@ def parse_message(self, message): elif mode == 0x46: # the monitor test mode only has a mode number - # the MID (mode 6's version of a PID) is repeated, + # the MID (mode 6's version of a PID) is needed, # and handled in the decoder message.data = message.data[1:] @@ -299,7 +297,7 @@ def parse_message(self, message): # [ Data ] # 49 04 01 35 36 30 32 38 39 34 39 41 43 00 00 00 00 00 00 message.data = message.data[2:] - + """ return True From 3c66e89ec86454e71691245431edb2ff46a76e7d Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 11 Sep 2016 00:13:06 -0400 Subject: [PATCH 489/569] tweaked legacy protocol to mimic CAN responses --- obd/protocols/protocol_legacy.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index 573b6c5c..d7d3b0e5 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -99,16 +99,25 @@ def parse_message(self, message): # legacy protocols have different re-assembly # procedures for different Modes + # ~~~~ + # NOTE: THERE ARE HACKS IN HERE to make some output compatible with CAN + # since CAN is the standard, and this is considered legacy, I'm + # fixing ugly inconsistencies between the two protocols here. + # ~~~~ + if mode == 0x43: # GET_DTC requests return frames with no PID or order bytes # accumulate all of the data, minus the Mode bytes of each frame # Ex. - # [ Frame ] + # insert faux-byte to mimic the CAN style DTC requests + # | + # [ | Frame ] # 48 6B 10 43 03 00 03 02 03 03 ck # 48 6B 10 43 03 04 00 00 00 00 ck # [ Data ] + message.data = bytearray([0x43, 0x00]) # forge the mode byte and CAN's DTC_count byte for f in frames: message.data += f.data[1:] @@ -117,11 +126,10 @@ def parse_message(self, message): # return data, excluding the mode/pid bytes # Ex. - # [ Frame ] + # [ Frame/Data ] # 48 6B 10 41 00 BE 7F B8 13 ck - # [ Data ] - message.data = frames[0].data[2:] + message.data = frames[0].data else: # len(frames) > 1: # generic multiline requests carry an order byte @@ -133,6 +141,11 @@ def parse_message(self, message): # 48 6B 10 49 02 03 30 30 52 35 ck # etc... [] [ Data ] + # becomes: + # 49 02 [] 00 00 00 31 44 34 47 50 30 30 52 35 + # | [ ] [ ] [ ] + # order byte is removed + # sort the frames by the order byte frames = sorted(frames, key=lambda f: f.data[2]) @@ -143,7 +156,13 @@ def parse_message(self, message): return False # now that they're in order, accumulate the data from each frame - for f in frames: + + # preserve the first frame's mode and PID bytes (for consistency with CAN) + frames[0].data.pop(2) # remove the sequence byte + message.data = frames[0].data + + # add the data from the remaining frames + for f in frames[1:]: message.data += f.data[3:] # loose the mode/pid/seq bytes return True From 3ea8a5788d2517930268588dfef7d85bcf4e2983 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 11 Sep 2016 00:29:56 -0400 Subject: [PATCH 490/569] retain the processing of CAN's DTC_Count handling --- obd/decoders.py | 2 +- obd/protocols/protocol_can.py | 35 ++++++++--------------------------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 99681b05..6ef93835 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -388,7 +388,7 @@ def dtc(messages): codes = [] d = [] for message in messages: - d += message.data + d += message.data[2:] # remove the mode and DTC_count bytes # look at data in pairs of bytes # looping through ENDING indices to avoid odd (invalid) code lengths diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 636be225..2ed58644 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -265,39 +265,20 @@ def parse_message(self, message): # chop to the correct size (as specified in the first frame) message.data = message.data[:ff[0].data_len] - """ - # chop off the Mode/PID bytes based on the mode number - mode = message.data[0] - if mode == 0x43: + # trim DTC requests based on DTC count + # this ISN'T in the decoder because the legacy protocols + # don't provide a DTC_count bytes, and instead, insert a 0x00 + # for consistency + + if message.data[0] == 0x43: # [] # 43 03 11 11 22 22 33 33 # [DTC] [DTC] [DTC] - # fetch the DTC count, and use it as a length code - num_dtc_bytes = message.data[1] * 2 - - # skip the PID byte and the DTC count, - message.data = message.data[2:][:num_dtc_bytes] + num_dtc_bytes = message.data[1] * 2 # each DTC is 2 bytes + message.data = message.data[:(num_dtc_bytes + 2)] # add 2 to account for mode/DTC_count bytes - elif mode == 0x46: - # the monitor test mode only has a mode number - # the MID (mode 6's version of a PID) is needed, - # and handled in the decoder - message.data = message.data[1:] - - else: - # skip the Mode and PID bytes - # - # single line response: - # [ Data ] - # 00 00 07 E8 06 41 00 BE 7F B8 13 - # - # OR, the data from a multiline response: - # [ Data ] - # 49 04 01 35 36 30 32 38 39 34 39 41 43 00 00 00 00 00 00 - message.data = message.data[2:] - """ return True From ec62a3873f32f7cdfcb18d30d7269909eb090867 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 11 Sep 2016 01:13:06 -0400 Subject: [PATCH 491/569] fixed protocol tests for CAN --- tests/test_protocol_can.py | 40 +++++++++----------------------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index 4392898f..3b0289ff 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -31,17 +31,17 @@ def test_single_frame(): r = p(["7E8 06 41 00 00 01 02 03"]) assert len(r) == 1 - check_message(r[0], 1, 0x0, [0x00, 0x01, 0x02, 0x03]) + check_message(r[0], 1, 0x0, [0x41, 0x00, 0x00, 0x01, 0x02, 0x03]) # minimum valid length r = p(["7E8 01 41"]) assert len(r) == 1 - check_message(r[0], 1, 0x0, []) + check_message(r[0], 1, 0x0, [0x41]) # maximum valid length r = p(["7E8 07 41 00 00 01 02 03 04"]) assert len(r) == 1 - check_message(r[0], 1, 0x0, [0x00, 0x01, 0x02, 0x03, 0x04]) + check_message(r[0], 1, 0x0, [0x41, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04]) # to short r = p(["7E8 01"]) @@ -89,7 +89,7 @@ def test_hex_straining(): # first message should be the valid, parsable hex message # NOTE: the parser happens to process the valid one's first - check_message(r[0], 1, 0x0, list(range(4))) + check_message(r[0], 1, 0x0, [0x41, 0x00, 0x00, 0x01, 0x02, 0x03]) # second message: invalid, non-parsable non-hex assert r[1].ecu == ECU.UNKNOWN @@ -109,7 +109,7 @@ def test_multi_ecu(): "7EA 06 41 00 00 01 02 03", ] - correct_data = list(range(4)) + correct_data = [0x41, 0x00, 0x00, 0x01, 0x02, 0x03] # seperate ECUs, single frames each r = p(test_case) @@ -138,7 +138,7 @@ def test_multi_line(): "7E8 23 12 13 14 15 16 17 18" ] - correct_data = list(range(25)) + correct_data = [0x49, 0x04] + list(range(25)) # in-order r = p(test_case) @@ -182,8 +182,8 @@ def test_multi_line_missing_frames(): def test_multi_line_mode_03(): """ Tests the special handling of mode 3 commands. - Namely, Mode 03 commands (GET_DTC) return no PID byte. - When frames are combined, the parser should account for this. + Namely, Mode 03 commands have a DTC count byte that is accounted for + in the protocol layer. """ for protocol in CAN_11_PROTOCOLS: @@ -194,34 +194,12 @@ def test_multi_line_mode_03(): "7E8 21 04 05 06 07 08 09 0A", ] - correct_data = list(range(8)) + correct_data = [0x43, 0x04] + list(range(8)) r = p(test_case) assert len(r) == 1 check_message(r[0], len(test_case), 0, correct_data) -def test_multi_line_mode_06(): - """ - Tests the special handling of mode 6 commands. - The parser should chop off only the Mode byte from the response. - """ - - for protocol in CAN_11_PROTOCOLS: - p = protocol([]) - - test_case = [ - "7E8 10 0A 46 01 01 0A 0B B0", - "7E8 21 0B B0 0B B0", - ] - - correct_data = [0x01, 0x01, 0x0A, 0x0B, 0xB0, 0x0B, 0xB0, 0x0B, 0xB0] - - r = p(test_case) - assert len(r) == 1 - check_message(r[0], len(test_case), 0, correct_data) - - - def test_can_29(): pass From 341cc77067a820d26206adffc7001c6ba7bc02d5 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 11 Sep 2016 01:17:00 -0400 Subject: [PATCH 492/569] fixed protocol tests for legacy --- tests/test_protocol_legacy.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_protocol_legacy.py b/tests/test_protocol_legacy.py index da74c882..d7791a1c 100644 --- a/tests/test_protocol_legacy.py +++ b/tests/test_protocol_legacy.py @@ -27,12 +27,12 @@ def test_single_frame(): # minimum valid length r = p(["48 6B 10 41 00 FF"]) assert len(r) == 1 - check_message(r[0], 1, 0x10, []) + check_message(r[0], 1, 0x10, [0x41, 0x00]) # maximum valid length r = p(["48 6B 10 41 00 00 01 02 03 04 FF"]) assert len(r) == 1 - check_message(r[0], 1, 0x10, list(range(5))) + check_message(r[0], 1, 0x10, [0x41, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04]) # to short r = p(["48 6B 10 41 FF"]) @@ -76,7 +76,7 @@ def test_hex_straining(): # first message should be the valid, parsable hex message # NOTE: the parser happens to process the valid one's first - check_message(r[0], 1, 0x10, list(range(4))) + check_message(r[0], 1, 0x10, [0x41, 0x00, 0x00, 0x01, 0x02, 0x03]) # second message: invalid, non-parsable non-hex assert r[1].ecu == ECU.UNKNOWN @@ -91,12 +91,12 @@ def test_multi_ecu(): test_case = [ - "48 6B 13 41 00 00 01 02 03 FF", + "48 6B 13 41 00 00 01 02 03 FF", "48 6B 10 41 00 00 01 02 03 FF", "48 6B 11 41 00 00 01 02 03 FF", ] - correct_data = list(range(4)) + correct_data = [0x41, 0x00, 0x00, 0x01, 0x02, 0x03] # seperate ECUs, single frames each r = p(test_case) @@ -124,7 +124,7 @@ def test_multi_line(): "48 6B 10 49 02 03 08 09 0A 0B FF", ] - correct_data = list(range(12)) + correct_data = [0x49, 0x02] + list(range(12)) # in-order r = p(test_case) @@ -167,8 +167,7 @@ def test_multi_line_missing_frames(): def test_multi_line_mode_03(): """ Tests the special handling of mode 3 commands. - Namely, Mode 03 commands (GET_DTC) return no PID byte. - When frames are combined, the parser should account for this. + An extra byte is fudged in to make the output look like CAN """ for protocol in LEGACY_PROTOCOLS: @@ -180,7 +179,8 @@ def test_multi_line_mode_03(): "48 6B 10 43 06 07 08 09 0A 0B FF", ] - correct_data = list(range(12)) # data is stitched in order recieved + correct_data = [0x43, 0x00] + list(range(12)) # data is stitched in order recieved + # ^^^^ this is an arbitrary value in the source code r = p(test_case) assert len(r) == 1 From 14bbb0fd78430213a9293406a107b8aaeeae797c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 11 Sep 2016 01:22:52 -0400 Subject: [PATCH 493/569] added mode/PID bytes in decoder tests --- tests/test_decoders.py | 170 ++++++++++++++++++++--------------------- 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 10d13500..a6e24d9f 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -25,7 +25,7 @@ def float_equals(va, vb): return values_match and units_match - +# NOTE: the prefix string is denoting the split between header (mode/PID bytes) and data def test_noop(): @@ -41,123 +41,123 @@ def test_raw_string(): assert d.raw_string([ Message([ Frame("A") ]), Message([ Frame("B") ]) ]) == "A\nB" def test_pid(): - assert d.pid(m("00000000")).bits == "00000000000000000000000000000000" - assert d.pid(m("F00AA00F")).bits == "11110000000010101010000000001111" - assert d.pid(m("11")).bits == "00010001" + assert d.pid(m("4100"+"00000000")).bits == "00000000000000000000000000000000" + assert d.pid(m("4100"+"F00AA00F")).bits == "11110000000010101010000000001111" + assert d.pid(m("4100"+"11")).bits == "00010001" def test_percent(): - assert d.percent(m("00")) == 0.0 * Unit.percent - assert d.percent(m("FF")) == 100.0 * Unit.percent + assert d.percent(m("4100"+"00")) == 0.0 * Unit.percent + assert d.percent(m("4100"+"FF")) == 100.0 * Unit.percent def test_percent_centered(): - assert d.percent_centered(m("00")) == -100.0 * Unit.percent - assert d.percent_centered(m("80")) == 0.0 * Unit.percent - assert float_equals(d.percent_centered(m("FF")), 99.2 * Unit.percent) + assert d.percent_centered(m("4100"+"00")) == -100.0 * Unit.percent + assert d.percent_centered(m("4100"+"80")) == 0.0 * Unit.percent + assert float_equals(d.percent_centered(m("4100"+"FF")), 99.2 * Unit.percent) def test_temp(): - assert d.temp(m("00")) == Unit.Quantity(-40, Unit.celsius) - assert d.temp(m("FF")) == Unit.Quantity(215, Unit.celsius) - assert d.temp(m("03E8")) == Unit.Quantity(960, Unit.celsius) + assert d.temp(m("4100"+"00")) == Unit.Quantity(-40, Unit.celsius) + assert d.temp(m("4100"+"FF")) == Unit.Quantity(215, Unit.celsius) + assert d.temp(m("4100"+"03E8")) == Unit.Quantity(960, Unit.celsius) def test_current_centered(): - assert d.current_centered(m("00000000")) == -128.0 * Unit.milliampere - assert d.current_centered(m("00008000")) == 0.0 * Unit.milliampere - assert d.current_centered(m("ABCD8000")) == 0.0 * Unit.milliampere # first 2 bytes are unused (should be disregarded) - assert float_equals(d.current_centered(m("0000FFFF")), 128.0 * Unit.milliampere) + assert d.current_centered(m("4100"+"00000000")) == -128.0 * Unit.milliampere + assert d.current_centered(m("4100"+"00008000")) == 0.0 * Unit.milliampere + assert d.current_centered(m("4100"+"ABCD8000")) == 0.0 * Unit.milliampere # first 2 bytes are unused (should be disregarded) + assert float_equals(d.current_centered(m("4100"+"0000FFFF")), 128.0 * Unit.milliampere) def test_sensor_voltage(): - assert d.sensor_voltage(m("0000")) == 0.0 * Unit.volt - assert d.sensor_voltage(m("FFFF")) == 1.275 * Unit.volt + assert d.sensor_voltage(m("4100"+"0000")) == 0.0 * Unit.volt + assert d.sensor_voltage(m("4100"+"FFFF")) == 1.275 * Unit.volt def test_sensor_voltage_big(): - assert d.sensor_voltage_big(m("00000000")) == 0.0 * Unit.volt - assert float_equals(d.sensor_voltage_big(m("00008000")), 4.0 * Unit.volt) - assert d.sensor_voltage_big(m("0000FFFF")) == 8.0 * Unit.volt - assert d.sensor_voltage_big(m("ABCD0000")) == 0.0 * Unit.volt # first 2 bytes are unused (should be disregarded) + assert d.sensor_voltage_big(m("4100"+"00000000")) == 0.0 * Unit.volt + assert float_equals(d.sensor_voltage_big(m("4100"+"00008000")), 4.0 * Unit.volt) + assert d.sensor_voltage_big(m("4100"+"0000FFFF")) == 8.0 * Unit.volt + assert d.sensor_voltage_big(m("4100"+"ABCD0000")) == 0.0 * Unit.volt # first 2 bytes are unused (should be disregarded) def test_fuel_pressure(): - assert d.fuel_pressure(m("00")) == 0 * Unit.kilopascal - assert d.fuel_pressure(m("80")) == 384 * Unit.kilopascal - assert d.fuel_pressure(m("FF")) == 765 * Unit.kilopascal + assert d.fuel_pressure(m("4100"+"00")) == 0 * Unit.kilopascal + assert d.fuel_pressure(m("4100"+"80")) == 384 * Unit.kilopascal + assert d.fuel_pressure(m("4100"+"FF")) == 765 * Unit.kilopascal def test_pressure(): - assert d.pressure(m("00")) == 0 * Unit.kilopascal - assert d.pressure(m("00")) == 0 * Unit.kilopascal + assert d.pressure(m("4100"+"00")) == 0 * Unit.kilopascal + assert d.pressure(m("4100"+"00")) == 0 * Unit.kilopascal def test_evap_pressure(): pass # TODO - #assert d.evap_pressure(m("0000")) == 0.0 * Unit.PA) + #assert d.evap_pressure(m("4100"+"0000")) == 0.0 * Unit.PA) def test_abs_evap_pressure(): - assert d.abs_evap_pressure(m("0000")) == 0 * Unit.kilopascal - assert d.abs_evap_pressure(m("FFFF")) == 327.675 * Unit.kilopascal + assert d.abs_evap_pressure(m("4100"+"0000")) == 0 * Unit.kilopascal + assert d.abs_evap_pressure(m("4100"+"FFFF")) == 327.675 * Unit.kilopascal def test_evap_pressure_alt(): - assert d.evap_pressure_alt(m("0000")) == -32767 * Unit.pascal - assert d.evap_pressure_alt(m("7FFF")) == 0 * Unit.pascal - assert d.evap_pressure_alt(m("FFFF")) == 32768 * Unit.pascal + assert d.evap_pressure_alt(m("4100"+"0000")) == -32767 * Unit.pascal + assert d.evap_pressure_alt(m("4100"+"7FFF")) == 0 * Unit.pascal + assert d.evap_pressure_alt(m("4100"+"FFFF")) == 32768 * Unit.pascal def test_timing_advance(): - assert d.timing_advance(m("00")) == -64.0 * Unit.degrees - assert d.timing_advance(m("FF")) == 63.5 * Unit.degrees + assert d.timing_advance(m("4100"+"00")) == -64.0 * Unit.degrees + assert d.timing_advance(m("4100"+"FF")) == 63.5 * Unit.degrees def test_inject_timing(): - assert d.inject_timing(m("0000")) == -210 * Unit.degrees - assert float_equals(d.inject_timing(m("FFFF")), 302 * Unit.degrees) + assert d.inject_timing(m("4100"+"0000")) == -210 * Unit.degrees + assert float_equals(d.inject_timing(m("4100"+"FFFF")), 302 * Unit.degrees) def test_max_maf(): - assert d.max_maf(m("00000000")) == 0 * Unit.grams_per_second - assert d.max_maf(m("FF000000")) == 2550 * Unit.grams_per_second - assert d.max_maf(m("00ABCDEF")) == 0 * Unit.grams_per_second # last 3 bytes are unused (should be disregarded) + assert d.max_maf(m("4100"+"00000000")) == 0 * Unit.grams_per_second + assert d.max_maf(m("4100"+"FF000000")) == 2550 * Unit.grams_per_second + assert d.max_maf(m("4100"+"00ABCDEF")) == 0 * Unit.grams_per_second # last 3 bytes are unused (should be disregarded) def test_fuel_rate(): - assert d.fuel_rate(m("0000")) == 0.0 * Unit.liters_per_hour - assert d.fuel_rate(m("FFFF")) == 3276.75 * Unit.liters_per_hour + assert d.fuel_rate(m("4100"+"0000")) == 0.0 * Unit.liters_per_hour + assert d.fuel_rate(m("4100"+"FFFF")) == 3276.75 * Unit.liters_per_hour def test_fuel_status(): - assert d.fuel_status(m("0100")) == ("Open loop due to insufficient engine temperature", "") - assert d.fuel_status(m("0800")) == ("Open loop due to system failure", "") - assert d.fuel_status(m("0808")) == ("Open loop due to system failure", + assert d.fuel_status(m("4100"+"0100")) == ("Open loop due to insufficient engine temperature", "") + assert d.fuel_status(m("4100"+"0800")) == ("Open loop due to system failure", "") + assert d.fuel_status(m("4100"+"0808")) == ("Open loop due to system failure", "Open loop due to system failure") - assert d.fuel_status(m("0008")) == ("", "Open loop due to system failure") - assert d.fuel_status(m("0000")) == None - assert d.fuel_status(m("0300")) == None - assert d.fuel_status(m("0303")) == None + assert d.fuel_status(m("4100"+"0008")) == ("", "Open loop due to system failure") + assert d.fuel_status(m("4100"+"0000")) == None + assert d.fuel_status(m("4100"+"0300")) == None + assert d.fuel_status(m("4100"+"0303")) == None def test_air_status(): - assert d.air_status(m("01")) == "Upstream" - assert d.air_status(m("08")) == "Pump commanded on for diagnostics" - assert d.air_status(m("03")) == None + assert d.air_status(m("4100"+"01")) == "Upstream" + assert d.air_status(m("4100"+"08")) == "Pump commanded on for diagnostics" + assert d.air_status(m("4100"+"03")) == None def test_fuel_type(): - assert d.fuel_type(m("00")) == "Not available" - assert d.fuel_type(m("17")) == "Bifuel running diesel" - assert d.fuel_type(m("18")) == None + assert d.fuel_type(m("4100"+"00")) == "Not available" + assert d.fuel_type(m("4100"+"17")) == "Bifuel running diesel" + assert d.fuel_type(m("4100"+"18")) == None def test_obd_compliance(): - assert d.obd_compliance(m("00")) == "Undefined" - assert d.obd_compliance(m("21")) == "Heavy Duty Euro OBD Stage VI (HD EOBD-IV)" - assert d.obd_compliance(m("22")) == None + assert d.obd_compliance(m("4100"+"00")) == "Undefined" + assert d.obd_compliance(m("4100"+"21")) == "Heavy Duty Euro OBD Stage VI (HD EOBD-IV)" + assert d.obd_compliance(m("4100"+"22")) == None def test_o2_sensors(): - assert d.o2_sensors(m("00")) == ((),(False, False, False, False), (False, False, False, False)) - assert d.o2_sensors(m("01")) == ((),(False, False, False, False), (False, False, False, True)) - assert d.o2_sensors(m("0F")) == ((),(False, False, False, False), (True, True, True, True)) - assert d.o2_sensors(m("F0")) == ((),(True, True, True, True), (False, False, False, False)) + assert d.o2_sensors(m("4100"+"00")) == ((),(False, False, False, False), (False, False, False, False)) + assert d.o2_sensors(m("4100"+"01")) == ((),(False, False, False, False), (False, False, False, True)) + assert d.o2_sensors(m("4100"+"0F")) == ((),(False, False, False, False), (True, True, True, True)) + assert d.o2_sensors(m("4100"+"F0")) == ((),(True, True, True, True), (False, False, False, False)) def test_o2_sensors_alt(): - assert d.o2_sensors_alt(m("00")) == ((),(False, False), (False, False), (False, False), (False, False)) - assert d.o2_sensors_alt(m("01")) == ((),(False, False), (False, False), (False, False), (False, True)) - assert d.o2_sensors_alt(m("0F")) == ((),(False, False), (False, False), (True, True), (True, True)) - assert d.o2_sensors_alt(m("F0")) == ((),(True, True), (True, True), (False, False), (False, False)) + assert d.o2_sensors_alt(m("4100"+"00")) == ((),(False, False), (False, False), (False, False), (False, False)) + assert d.o2_sensors_alt(m("4100"+"01")) == ((),(False, False), (False, False), (False, False), (False, True)) + assert d.o2_sensors_alt(m("4100"+"0F")) == ((),(False, False), (False, False), (True, True), (True, True)) + assert d.o2_sensors_alt(m("4100"+"F0")) == ((),(True, True), (True, True), (False, False), (False, False)) def test_aux_input_status(): - assert d.aux_input_status(m("00")) == False - assert d.aux_input_status(m("80")) == True + assert d.aux_input_status(m("4100"+"00")) == False + assert d.aux_input_status(m("4100"+"80")) == True def test_absolute_load(): - assert d.absolute_load(m("0000")) == 0 * Unit.percent - assert d.absolute_load(m("FFFF")) == 25700 * Unit.percent + assert d.absolute_load(m("4100"+"0000")) == 0 * Unit.percent + assert d.absolute_load(m("4100"+"FFFF")) == 25700 * Unit.percent def test_elm_voltage(): # these aren't parsed as standard hex messages, so manufacture our own @@ -166,7 +166,7 @@ def test_elm_voltage(): assert d.elm_voltage([ Message([ Frame("12ABCD") ]) ]) == None def test_status(): - status = d.status(m("8307FF00")) + status = d.status(m("4100"+"8307FF00")) assert status.MIL assert status.DTC_count == 3 assert status.ignition_type == "spark" @@ -188,7 +188,7 @@ def test_status(): assert status.__dict__[name].complete # a different test - status = d.status(m("00790303")) + status = d.status(m("4100"+"00790303")) assert not status.MIL assert status.DTC_count == 0 assert status.ignition_type == "compression" @@ -227,36 +227,36 @@ def test_status(): def test_single_dtc(): - assert d.single_dtc(m("0104")) == ("P0104", "Mass or Volume Air Flow Circuit Intermittent") - assert d.single_dtc(m("4123")) == ("C0123", "") # reverse back into correct bit-order - assert d.single_dtc(m("01")) == None - assert d.single_dtc(m("010400")) == None + assert d.single_dtc(m("4100"+"0104")) == ("P0104", "Mass or Volume Air Flow Circuit Intermittent") + assert d.single_dtc(m("4100"+"4123")) == ("C0123", "") # reverse back into correct bit-order + assert d.single_dtc(m("4100"+"01")) == None + assert d.single_dtc(m("4100"+"010400")) == None def test_dtc(): - assert d.dtc(m("0104")) == [ + assert d.dtc(m("4100"+"0104")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ] # multiple codes - assert d.dtc(m("010480034123")) == [ + assert d.dtc(m("4100"+"010480034123")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ("B0003", ""), # unknown error codes return empty strings ("C0123", ""), ] # invalid code lengths are dropped - assert d.dtc(m("0104800341")) == [ + assert d.dtc(m("4100"+"0104800341")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ("B0003", ""), ] # 0000 codes are dropped - assert d.dtc(m("000001040000")) == [ + assert d.dtc(m("4100"+"000001040000")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ] # test multiple messages - assert d.dtc(m("0104") + m("8003") + m("0000")) == [ + assert d.dtc(m("4100"+"0104") + m("4100"+"8003") + m("4100"+"0000")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ("B0003", ""), ] @@ -264,7 +264,7 @@ def test_dtc(): def test_monitor(): # single test ----------------------------------------- # [ test ] - v = d.monitor(m("01010A0BB00BB00BB0")) + v = d.monitor(m("41"+"01010A0BB00BB00BB0")) assert len(v) == 1 # 1 test result # make sure we can look things up by name and TID @@ -279,7 +279,7 @@ def test_monitor(): # multiple tests -------------------------------------- # [ test ][ test ][ test ] - v = d.monitor(m("01010A0BB00BB00BB00105100048000000640185240096004BFFFF")) + v = d.monitor(m("41"+"01010A0BB00BB00BB00105100048000000640185240096004BFFFF")) assert len(v) == 3 # 3 test results # make sure we can look things up by name and TID @@ -305,7 +305,7 @@ def test_monitor(): # truncate incomplete tests ---------------------------- # [ test ][junk] - v = d.monitor(m("01010A0BB00BB00BB0ABCDEF")) + v = d.monitor(m("41"+"01010A0BB00BB00BB0ABCDEF")) assert len(v) == 1 # 1 test result # make sure we can look things up by name and TID @@ -319,7 +319,7 @@ def test_monitor(): assert float_equals(v[0x01].max, 365 * Unit.millivolt) # truncate incomplete tests ---------------------------- - v = d.monitor(m("01010A0BB00BB00B")) + v = d.monitor(m("41"+"01010A0BB00BB00B")) assert len(v) == 0 # no valid tests # make sure that the standard tests are null From bee7ed778d93af349af28e61f328a8926929f4c2 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 11 Sep 2016 01:56:30 -0400 Subject: [PATCH 494/569] fixed OBDCommand tests for byte trimming --- tests/test_OBDCommand.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index f8727c07..f95b4d36 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -46,24 +46,24 @@ def test_clone(): def test_call(): p = SAE_J1850_PWM(["48 6B 10 41 00 FF FF FF FF AA"]) # train the ecu_map to identify the engine - messages = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) # parse valid data into response object + messages = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) # parse valid data into response object print(messages[0].data) # valid response size - cmd = OBDCommand("", "", b"0123", 4, noop, ECU.ENGINE) + cmd = OBDCommand("", "", b"0123", 6, noop, ECU.ENGINE) r = cmd(messages) - assert r.value == b'\xBE\x1F\xB8\x11' + assert r.value == bytearray([0x41, 0x00, 0xBE, 0x1F, 0xB8, 0x11]) # response too short (pad) - cmd = OBDCommand("", "", b"0123", 5, noop, ECU.ENGINE) + cmd = OBDCommand("", "", b"0123", 7, noop, ECU.ENGINE) r = cmd(messages) - assert r.value == b'\xBE\x1F\xB8\x11\x00' + assert r.value == bytearray([0x41, 0x00, 0xBE, 0x1F, 0xB8, 0x11, 0x00]) # response too long (clip) - cmd = OBDCommand("", "", b"0123", 3, noop, ECU.ENGINE) + cmd = OBDCommand("", "", b"0123", 5, noop, ECU.ENGINE) r = cmd(messages) - assert r.value == b'\xBE\x1F\xB8' + assert r.value == bytearray([0x41, 0x00, 0xBE, 0x1F, 0xB8]) From 2764db67268f42ca575328212e9c7fc4ef4ffc5b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 11 Sep 2016 02:16:56 -0400 Subject: [PATCH 495/569] updated protocol readme with expanded data field --- obd/protocols/README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/obd/protocols/README.md b/obd/protocols/README.md index e5b9c193..a29fc822 100644 --- a/obd/protocols/README.md +++ b/obd/protocols/README.md @@ -1,20 +1,18 @@ Notes ----- -Each protocol object is callable, and accepts a list of raw input strings, and returns a list of parsed `Message` objects. The `data` field will contain a bytearray, corresponding to all relevant data returned by the command. - -*Note: `Message.data` does not refer to the full data field of a message. Things like PCI/Mode/PID bytes are often removed. If you want to see these fields, use `Frame.data` for the full (per-spec) data field.* +This code is meant to abstract the transport and physical layers of an OBD-II connection. Each protocol is a callable object, and accepts a list of strings as input (from the adapter), and returns a list of parsed `Message` objects. The `Message.data` field will contain a bytearray, corresponding to the application layer data returned by the command. This implementation is specific to the formatting of the ELM327 chip inside the adapter. For example, these are the resultant `Message.data` fields for some single frame messages: ``` A CAN Message: 7E8 06 41 00 BE 7F B8 13 - [ data ] + [ data ] A J1850 Message: 48 6B 10 41 00 BE 7F B8 13 FF - [ data ] + [ data ] ``` The parsing itself (invoking `__call__`) is stateless. The only stateful part of a `Protocol` is the `ECU_Map`. These objects correlate OBD transmitter IDs (`tx_id`'s) with the various ECUs in the car. This way, `Message` objects can be marked with ECU constants such as: From 7d4e6244d6ee8f065a0dab0742c8315fb9c5de78 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 11 Sep 2016 14:04:50 -0400 Subject: [PATCH 496/569] Message.data, not Frame.data --- obd/protocols/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/protocols/README.md b/obd/protocols/README.md index a29fc822..693cf141 100644 --- a/obd/protocols/README.md +++ b/obd/protocols/README.md @@ -31,13 +31,13 @@ All protocol objects must implement the following: #### parse_frame(self, frame) -Recieves a single `Frame` object with `Frame.raw` preloaded with the raw line recieved from the car (in string form). This function is responsible for parsing `Frame.raw`, and filling the remaining fields in the `Frame` object. If the frame is invalid, or the parse fails, this function should return `False`, and the frame will be dropped. +Recieves a single `Frame` object with `Frame.raw` preloaded with the raw line recieved from the car (in string form). This function is responsible for parsing `Frame.raw` into a bytearray, and filling the remaining fields in the `Frame` object. If the frame is invalid, or the parse fails, this function should return `False`, and the frame will be dropped. ---------------------------------------- #### parse_message(self, message) -Recieves a single `Message` object with `Message.frames` preloaded with a list of `Frame` objects. This function is responsible for assembling the frames into the `Frame.data` field in the `Message` object. This is where multi-line responses are assembled. If the message is found to be invalid, this function should return `False`, and the entire message will be dropped. +Recieves a single `Message` object with `Message.frames` preloaded with a list of `Frame` objects. This function is responsible for assembling the frames into the `Message.data` field in the `Message` object. This is where multi-line responses are assembled. If the message is found to be invalid, this function should return `False`, and the entire message will be dropped. ---------------------------------------- From cf88cbd0118ec23f84ea9445e56f9048f22a2c7c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Wed, 14 Sep 2016 18:40:55 -0400 Subject: [PATCH 497/569] dropping support for python 3.2 python 3.2 has an ugly problem where Unicode strings (the default in python 3) couldn't be used in binascii functions. This broke a lot of calls to unhexlify. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ad22f32f..3bbfeffd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: python python: - 2.7 - - 3.2 - 3.3 - 3.4 - 3.5 From 022a81b93582e20918475d38aa4bb7aac784c3ab Mon Sep 17 00:00:00 2001 From: Jeff McGehee Date: Fri, 24 Nov 2017 00:04:11 -0600 Subject: [PATCH 498/569] Make connection timeout configurable Why: * the hard-coded value of 0.1 was making it impossible to connect with OSX, and possibly in other cases. This change addresses the need by: * Adding `conn_timeout` kwarg to OBD and Async, which defaults to 0.1, but can be configured to suit the user's neeeds. Other observations: * There were a few failing tests locally (python3.6 on OSX 10.12.6) when I cloned the repo, these tests are still failing. I may look into these next and submit another PR. --- obd/async.py | 6 ++++-- obd/elm327.py | 5 +++-- obd/obd.py | 10 +++++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/obd/async.py b/obd/async.py index 335c5872..f3eac4b3 100644 --- a/obd/async.py +++ b/obd/async.py @@ -45,8 +45,10 @@ class Async(OBD): Specialized for asynchronous value reporting. """ - def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True): - super(Async, self).__init__(portstr, baudrate, protocol, fast) + def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True, + conn_timeout=0.1): + super(Async, self).__init__(portstr, baudrate, protocol, fast, + conn_timeout) self.__commands = {} # key = OBDCommand, value = Response self.__callbacks = {} # key = OBDCommand, value = list of Functions self.__thread = None diff --git a/obd/elm327.py b/obd/elm327.py index c469975b..744490f1 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -103,7 +103,7 @@ class ELM327: - def __init__(self, portname, baudrate, protocol): + def __init__(self, portname, baudrate, protocol, conn_timeout): """Initializes port by resetting device and gettings supported PIDs. """ logger.info("Initializing ELM327: PORT=%s BAUD=%s PROTOCOL=%s" % @@ -116,6 +116,7 @@ def __init__(self, portname, baudrate, protocol): self.__status = OBDStatus.NOT_CONNECTED self.__port = None self.__protocol = UnknownProtocol([]) + self.conn_timeout = conn_timeout # ------------- open port ------------- @@ -276,7 +277,7 @@ def auto_baudrate(self): # before we change the timout, save the "normal" value timeout = self.__port.timeout - self.__port.timeout = 0.1 # we're only talking with the ELM, so things should go quickly + self.__port.timeout = self.conn_timeout # we're only talking with the ELM, so things should go quickly for baud in self._TRY_BAUDS: self.__port.baudrate = baud diff --git a/obd/obd.py b/obd/obd.py index 221725b1..964485a8 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -48,10 +48,12 @@ class OBD(object): with it's assorted commands/sensors. """ - def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True): + def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True, + conn_timeout=0.1): self.interface = None self.supported_commands = set(commands.base_commands()) self.fast = fast # global switch for disabling optimizations + self.conn_timeout = conn_timeout self.__last_command = b"" # used for running the previous command with a CR self.__frame_counts = {} # keeps track of the number of return frames for each command @@ -77,13 +79,15 @@ def __connect(self, portstr, baudrate, protocol): for port in portnames: logger.info("Attempting to use port: " + str(port)) - self.interface = ELM327(port, baudrate, protocol) + self.interface = ELM327(port, baudrate, protocol, + self.conn_timeout) if self.interface.status() >= OBDStatus.ELM_CONNECTED: break # success! stop searching for serial else: logger.info("Explicit port defined") - self.interface = ELM327(portstr, baudrate, protocol) + self.interface = ELM327(portstr, baudrate, protocol, + self.conn_timeout) # if the connection failed, close it if self.interface.status() == OBDStatus.NOT_CONNECTED: From 1713ee13687d41cb9e8c0ccd718a5f994e654400 Mon Sep 17 00:00:00 2001 From: Jeff McGehee Date: Sat, 2 Jun 2018 10:56:46 -0400 Subject: [PATCH 499/569] change to --- obd/async.py | 4 ++-- obd/elm327.py | 6 +++--- obd/obd.py | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/obd/async.py b/obd/async.py index f3eac4b3..fc1af5ad 100644 --- a/obd/async.py +++ b/obd/async.py @@ -46,9 +46,9 @@ class Async(OBD): """ def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True, - conn_timeout=0.1): + timeout=0.1): super(Async, self).__init__(portstr, baudrate, protocol, fast, - conn_timeout) + timeout) self.__commands = {} # key = OBDCommand, value = Response self.__callbacks = {} # key = OBDCommand, value = list of Functions self.__thread = None diff --git a/obd/elm327.py b/obd/elm327.py index 744490f1..b9e2b196 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -103,7 +103,7 @@ class ELM327: - def __init__(self, portname, baudrate, protocol, conn_timeout): + def __init__(self, portname, baudrate, protocol, timeout): """Initializes port by resetting device and gettings supported PIDs. """ logger.info("Initializing ELM327: PORT=%s BAUD=%s PROTOCOL=%s" % @@ -116,7 +116,7 @@ def __init__(self, portname, baudrate, protocol, conn_timeout): self.__status = OBDStatus.NOT_CONNECTED self.__port = None self.__protocol = UnknownProtocol([]) - self.conn_timeout = conn_timeout + self.timeout = timeout # ------------- open port ------------- @@ -277,7 +277,7 @@ def auto_baudrate(self): # before we change the timout, save the "normal" value timeout = self.__port.timeout - self.__port.timeout = self.conn_timeout # we're only talking with the ELM, so things should go quickly + self.__port.timeout = self.timeout # we're only talking with the ELM, so things should go quickly for baud in self._TRY_BAUDS: self.__port.baudrate = baud diff --git a/obd/obd.py b/obd/obd.py index 964485a8..84dd334f 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -49,11 +49,11 @@ class OBD(object): """ def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True, - conn_timeout=0.1): + timeout=0.1): self.interface = None self.supported_commands = set(commands.base_commands()) self.fast = fast # global switch for disabling optimizations - self.conn_timeout = conn_timeout + self.timeout = timeout self.__last_command = b"" # used for running the previous command with a CR self.__frame_counts = {} # keeps track of the number of return frames for each command @@ -80,14 +80,14 @@ def __connect(self, portstr, baudrate, protocol): for port in portnames: logger.info("Attempting to use port: " + str(port)) self.interface = ELM327(port, baudrate, protocol, - self.conn_timeout) + self.timeout) if self.interface.status() >= OBDStatus.ELM_CONNECTED: break # success! stop searching for serial else: logger.info("Explicit port defined") self.interface = ELM327(portstr, baudrate, protocol, - self.conn_timeout) + self.timeout) # if the connection failed, close it if self.interface.status() == OBDStatus.NOT_CONNECTED: From 301827813c7f6aee1cf5d776986a7126dfbbeaf9 Mon Sep 17 00:00:00 2001 From: Jeff McGehee Date: Sat, 2 Jun 2018 11:01:11 -0400 Subject: [PATCH 500/569] update docs --- docs/Connections.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/Connections.md b/docs/Connections.md index a6dbfa94..1575e36e 100644 --- a/docs/Connections.md +++ b/docs/Connections.md @@ -20,7 +20,7 @@ connection = obd.OBD(ports[0]) # connect to the first port in the list
-### OBD(portstr=None, baudrate=None, protocol=None, fast=True): +### OBD(portstr=None, baudrate=None, protocol=None, fast=True, timeout=0.1): `portstr`: The UNIX device file or Windows COM Port for your adapter. The default value (`None`) will auto select a port. @@ -35,6 +35,8 @@ connection = obd.OBD(ports[0]) # connect to the first port in the list Disabling fast mode will guarantee that python-OBD outputs the unaltered command for every request. +`timeout`: Specifies the connection timeout. +
--- From fb811922c733839aa196d30c554684cb8c877df2 Mon Sep 17 00:00:00 2001 From: Bruno Produit Date: Sun, 24 Jun 2018 23:51:04 +0300 Subject: [PATCH 501/569] Changed line to use pyserial's "serial_for_url" instead of "Serial" for compatibility --- obd/elm327.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/elm327.py b/obd/elm327.py index c469975b..d477e400 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -120,7 +120,7 @@ def __init__(self, portname, baudrate, protocol): # ------------- open port ------------- try: - self.__port = serial.Serial(portname, \ + self.__port = serial.serial_for_url(portname, \ parity = serial.PARITY_NONE, \ stopbits = 1, \ bytesize = 8, From e530ef8803088c0d5069eed55451d7b338babaea Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jun 2018 16:20:08 -0700 Subject: [PATCH 502/569] Timeout has a unit of seconds --- docs/Connections.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Connections.md b/docs/Connections.md index 1575e36e..7d85c1e2 100644 --- a/docs/Connections.md +++ b/docs/Connections.md @@ -35,7 +35,7 @@ connection = obd.OBD(ports[0]) # connect to the first port in the list Disabling fast mode will guarantee that python-OBD outputs the unaltered command for every request. -`timeout`: Specifies the connection timeout. +`timeout`: Specifies the connection timeout in seconds.
From a9eb5af9860ec11ea22c347cc70dd3c04fdd8b6b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jun 2018 17:11:43 -0700 Subject: [PATCH 503/569] Fixed test_obdsim.py failure The async connection test was failing, due to the first RPM query returning null. This was due to 36784bffc2ca50bad7b0f7941189eda8a9865467, which has python-OBD learning the number of messages to expect (rather than a dumb ECU count, which may not be true for all commands). In this case, "fast" mode will not attach a postfix message count, and will opt to incur the ELM's timeout on the first query only, in order to learn the number of replies. This extra firt-message initialization cost is more correct, but resulted in a timeout needing to be tuned in the unit test. --- tests/test_obdsim.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_obdsim.py b/tests/test_obdsim.py index d94862dc..1489a0fd 100644 --- a/tests/test_obdsim.py +++ b/tests/test_obdsim.py @@ -3,8 +3,12 @@ import pytest from obd import commands, Unit -STANDARD_WAIT_TIME = 0.2 - +# NOTE: This is purposefully tuned slightly higher than the ELM's default +# message timeout of 200 milliseconds. This prevents us from +# inadvertently marking the first query of an async connection as +# null, since it may be the case that the first transaction incurs the +# ELM's internal timeout. +STANDARD_WAIT_TIME = 0.3 @pytest.fixture(scope="module") def obd(request): From 98d996c345a6d0da68ad36037c335484d07719bd Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 24 Jun 2018 17:31:36 -0700 Subject: [PATCH 504/569] Updated custom command docs to reflect the presence of the mode/PID bytes --- docs/Custom Commands.md | 7 ++++--- obd/commands.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/Custom Commands.md b/docs/Custom Commands.md index 71fe1cb2..c5f2c68a 100644 --- a/docs/Custom Commands.md +++ b/docs/Custom Commands.md @@ -22,14 +22,15 @@ from obd.utils import bytes_to_int def rpm(messages): """ decoder for RPM messages """ - d = messages[0].data + d = messages[0].data # only operate on a single message + d = d[2:] # chop off mode and PID bytes v = bytes_to_int(d) / 4.0 # helper function for converting byte arrays to ints return v * Unit.RPM # construct a Pint Quantity c = OBDCommand("RPM", \ # name "Engine RPM", \ # description b"010C", \ # command - 2, \ # number of return bytes to expect + 4, \ # number of return bytes to expect rpm, \ # decoding function ECU.ENGINE, \ # (optional) ECU filter True) # (optional) allow a "01" to be added for speed @@ -66,7 +67,7 @@ def (): return ``` -The return value of your decoder will be loaded into the `OBDResponse.value` field. Decoders are given a list of `Message` objects as an argument. If your decoder is called, this list is garaunteed to have at least one message object. Each `Message` object has a `data` property, which holds a parsed bytearray, and is also garauteed to have the number of bytes specified by the command. +The return value of your decoder will be loaded into the `OBDResponse.value` field. Decoders are given a list of `Message` objects as an argument. If your decoder is called, this list is garaunteed to have at least one message object. Each `Message` object has a `data` property, which holds a parsed bytearray, and is also garauteed to have the number of bytes specified by the command. This bytearray includes any mode and PID bytes in the vehicle's response. *NOTE: If you are transitioning from an older version of Python-OBD (where decoders were given raw hex strings as arguments), you can use the `Message.hex()` function as a patch.* diff --git a/obd/commands.py b/obd/commands.py index 0f511009..967b304c 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -86,7 +86,7 @@ # name description cmd bytes decoder ECU fast OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , b"0120", 6, pid, ECU.ENGINE, True), - OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , b"0121", 4, uas(0x25), ECU.ENGINE, True), + OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , b"0121", 4, uas(0x25), ECU.ENGINE, True), OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , b"0122", 4, uas(0x19), ECU.ENGINE, True), OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , b"0123", 4, uas(0x1B), ECU.ENGINE, True), OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , b"0124", 6, sensor_voltage_big, ECU.ENGINE, True), @@ -102,7 +102,7 @@ OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , b"012E", 3, percent, ECU.ENGINE, True), OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , b"012F", 3, percent, ECU.ENGINE, True), OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , b"0130", 3, uas(0x01), ECU.ENGINE, True), - OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , b"0131", 4, uas(0x25), ECU.ENGINE, True), + OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , b"0131", 4, uas(0x25), ECU.ENGINE, True), OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , b"0132", 4, evap_pressure, ECU.ENGINE, True), OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , b"0133", 3, pressure, ECU.ENGINE, True), OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , b"0134", 6, current_centered, ECU.ENGINE, True), From 81fc7e559983ed9dfd2ab4b4c710033b08e1ed40 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Sat, 1 Sep 2018 11:21:19 -0700 Subject: [PATCH 505/569] Ignore non-decodeable data from the interface --- obd/elm327.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/elm327.py b/obd/elm327.py index 65969572..8e7c86ae 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -463,7 +463,7 @@ def __read(self): buffer = buffer[:-1] # convert bytes into a standard string - string = buffer.decode() + string = buffer.decode("utf-8", "ignore") # splits into lines while removing empty lines and trailing spaces lines = [ s.strip() for s in re.split("[\r\n]", string) if bool(s) ] From cc674d475b876b3d99745ccfb07f40eb2c5cf42d Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Fri, 2 Nov 2018 06:53:44 -0700 Subject: [PATCH 506/569] obd: protocols: Itterate over ECU frames in ECU order Python 3.6 and newer now itterates over dictionaries in the order the elements were added instead of over the keys like previously. To fix the tests lets ensure that we itterate over the order of the keys instead. Signed-off-by: Alistair Francis --- obd/protocols/protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 40619f89..62d9f825 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -207,7 +207,7 @@ def __call__(self, lines): # parse frames into whole messages messages = [] - for ecu in frames_by_ECU: + for ecu in sorted(frames_by_ECU.keys()): # new message object with a copy of the raw data # and frames addressed for this ecu From 5fee8034d865c1b369ef9949bfc031538b4b826f Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Wed, 31 Oct 2018 23:04:59 -0700 Subject: [PATCH 507/569] Fix the async problems with Python 3.7 As Python 3.7 has reserved the name async let's change the names in the source to fix the syntax errors. Signed-off-by: Alistair Francis --- obd/__init__.py | 2 +- obd/{async.py => asynchronous.py} | 0 tests/test_obdsim.py | 76 +++++++++++++++---------------- 3 files changed, 39 insertions(+), 39 deletions(-) rename obd/{async.py => asynchronous.py} (100%) diff --git a/obd/__init__.py b/obd/__init__.py index 8103db45..a013837d 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -39,7 +39,7 @@ from .__version__ import __version__ from .obd import OBD -from .async import Async +from .asynchronous import Async from .commands import commands from .OBDCommand import OBDCommand from .OBDResponse import OBDResponse diff --git a/obd/async.py b/obd/asynchronous.py similarity index 100% rename from obd/async.py rename to obd/asynchronous.py diff --git a/tests/test_obdsim.py b/tests/test_obdsim.py index 1489a0fd..d3319c45 100644 --- a/tests/test_obdsim.py +++ b/tests/test_obdsim.py @@ -19,7 +19,7 @@ def obd(request): @pytest.fixture(scope="module") -def async(request): +def asynchronous(request): """provides an OBD *Async* connection object for obdsim""" import obd port = request.config.getoption("--port") @@ -50,18 +50,18 @@ def test_rpm(obd): @pytest.mark.skipif(not pytest.config.getoption("--port"), reason="needs --port= to run") -def test_async_query(async): +def test_async_query(asynchronous): rs = [] - async.watch(commands.RPM) - async.start() + asynchronous.watch(commands.RPM) + asynchronous.start() for i in range(5): time.sleep(STANDARD_WAIT_TIME) - rs.append(async.query(commands.RPM)) + rs.append(asynchronous.query(commands.RPM)) - async.stop() - async.unwatch_all() + asynchronous.stop() + asynchronous.unwatch_all() # make sure we got data assert(len(rs) > 0) @@ -70,14 +70,14 @@ def test_async_query(async): @pytest.mark.skipif(not pytest.config.getoption("--port"), reason="needs --port= to run") -def test_async_callback(async): +def test_async_callback(asynchronous): rs = [] - async.watch(commands.RPM, callback=rs.append) - async.start() + asynchronous.watch(commands.RPM, callback=rs.append) + asynchronous.start() time.sleep(STANDARD_WAIT_TIME) - async.stop() - async.unwatch_all() + asynchronous.stop() + asynchronous.unwatch_all() # make sure we got data assert(len(rs) > 0) @@ -86,44 +86,44 @@ def test_async_callback(async): @pytest.mark.skipif(not pytest.config.getoption("--port"), reason="needs --port= to run") -def test_async_paused(async): +def test_async_paused(asynchronous): - assert(not async.running) - async.watch(commands.RPM) - async.start() - assert(async.running) + assert(not asynchronous.running) + asynchronous.watch(commands.RPM) + asynchronous.start() + assert(asynchronous.running) - with async.paused() as was_running: - assert(not async.running) + with asynchronous.paused() as was_running: + assert(not asynchronous.running) assert(was_running) - assert(async.running) - async.stop() - assert(not async.running) + assert(asynchronous.running) + asynchronous.stop() + assert(not asynchronous.running) @pytest.mark.skipif(not pytest.config.getoption("--port"), reason="needs --port= to run") -def test_async_unwatch(async): +def test_async_unwatch(asynchronous): watched_rs = [] unwatched_rs = [] - async.watch(commands.RPM) - async.start() + asynchronous.watch(commands.RPM) + asynchronous.start() for i in range(5): time.sleep(STANDARD_WAIT_TIME) - watched_rs.append(async.query(commands.RPM)) + watched_rs.append(asynchronous.query(commands.RPM)) - with async.paused(): - async.unwatch(commands.RPM) + with asynchronous.paused(): + asynchronous.unwatch(commands.RPM) for i in range(5): time.sleep(STANDARD_WAIT_TIME) - unwatched_rs.append(async.query(commands.RPM)) + unwatched_rs.append(asynchronous.query(commands.RPM)) - async.stop() + asynchronous.stop() # the watched commands assert(len(watched_rs) > 0) @@ -136,22 +136,22 @@ def test_async_unwatch(async): @pytest.mark.skipif(not pytest.config.getoption("--port"), reason="needs --port= to run") -def test_async_unwatch_callback(async): +def test_async_unwatch_callback(asynchronous): a_rs = [] b_rs = [] - async.watch(commands.RPM, callback=a_rs.append) - async.watch(commands.RPM, callback=b_rs.append) + asynchronous.watch(commands.RPM, callback=a_rs.append) + asynchronous.watch(commands.RPM, callback=b_rs.append) - async.start() + asynchronous.start() time.sleep(STANDARD_WAIT_TIME) - with async.paused(): - async.unwatch(commands.RPM, callback=b_rs.append) + with asynchronous.paused(): + asynchronous.unwatch(commands.RPM, callback=b_rs.append) time.sleep(STANDARD_WAIT_TIME) - async.stop() - async.unwatch_all() + asynchronous.stop() + asynchronous.unwatch_all() assert(all([ good_rpm_response(r) for r in a_rs + b_rs ])) assert(len(a_rs) > len(b_rs)) From f82c38103f3c8465490568fae1375a4bf8c19e1e Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Wed, 31 Oct 2018 22:39:52 -0700 Subject: [PATCH 508/569] travis.yml: Add Python 3.6 and 3.7 testing Add support for testing Python 3.6 and 3.7. This requires updating the Ubuntu enviroment used for testing. This means we can no longer test Python 3.3, but as it is end of life that isn't too important. Signed-off-by: Alistair Francis --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3bbfeffd..40f73a8f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,12 @@ language: python +dist: xenial python: - 2.7 - - 3.3 - 3.4 - 3.5 + - 3.6 + - 3.7 script: - python setup.py install From 88af33c1164afb295df59cde40aa9547f85a8c7a Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Sun, 4 Nov 2018 21:44:48 +0000 Subject: [PATCH 509/569] obd/commands.py: Remove isinstance(...unicode) for Python 3 In Python 3 all strings are unicode and this check results in an error as unicode is not defined. Remove the check as it isn't needed. At the same time keep the basestring check (str and unicode) for Python 2. Signed-off-by: Alistair Francis --- obd/commands.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/obd/commands.py b/obd/commands.py index 967b304c..993e4455 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -342,7 +342,9 @@ def __getitem__(self, key): if isinstance(key, int): return self.modes[key] - elif isinstance(key, str) or isinstance(key, unicode): + elif isinstance(key, str): + return self.__dict__[key] + elif sys.version_info[0] == 2 and isinstance(key, basestring): return self.__dict__[key] else: logger.warning("OBD commands can only be retrieved by PID value or dict name") From 7b6bcd3b66d8952a263106d2216a7f43071e0e36 Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Mon, 5 Nov 2018 00:46:41 +0000 Subject: [PATCH 510/569] obd/commands.py: Improve new multiversion isinstance() Improve the new isintance() unicode testing to better support Python 2 and Python 3. Signed-off-by: Alistair Francis --- obd/commands.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/obd/commands.py b/obd/commands.py index 993e4455..cf0841aa 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -340,11 +340,14 @@ def __getitem__(self, key): obd.commands[1][12] # mode 1, PID 12 (RPM) """ + try: + basestring + except NameError: + basestring = str + if isinstance(key, int): return self.modes[key] - elif isinstance(key, str): - return self.__dict__[key] - elif sys.version_info[0] == 2 and isinstance(key, basestring): + elif isinstance(key, basestring): return self.__dict__[key] else: logger.warning("OBD commands can only be retrieved by PID value or dict name") From cc36562197a466a84edeeaf7c2db699b63d40230 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 4 Nov 2018 21:43:30 -0800 Subject: [PATCH 511/569] bumped to v0.7.0 --- obd/__version__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/__version__.py b/obd/__version__.py index 7a2d0cd1..c2b99956 100644 --- a/obd/__version__.py +++ b/obd/__version__.py @@ -1,2 +1,2 @@ -__version__ = '0.6.1' +__version__ = '0.7.0' diff --git a/setup.py b/setup.py index 60835a1b..e5f83401 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="obd", - version="0.6.1", + version="0.7.0", description=("Serial module for handling live sensor data from a vehicle's OBD-II port"), classifiers=[ "Operating System :: POSIX :: Linux", From 4a2ece5aa0e2dba4f47cdfc43323f72641c3471c Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 4 Nov 2018 22:25:43 -0800 Subject: [PATCH 512/569] Use the new long_description param in setup.py for a prettier PyPi page --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index e5f83401..4fdacc11 100644 --- a/setup.py +++ b/setup.py @@ -3,10 +3,15 @@ from setuptools import setup, find_packages +with open("README.md", "r") as readme: + long_description = readme.read() + setup( name="obd", version="0.7.0", description=("Serial module for handling live sensor data from a vehicle's OBD-II port"), + long_description=long_description, + long_description_content_type="text/markdown", classifiers=[ "Operating System :: POSIX :: Linux", "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", From 66e15e3b182bbf4f5c8c0921fc31b9b30748eca5 Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Wed, 7 Nov 2018 15:24:03 +0000 Subject: [PATCH 513/569] obd: decoders: Ensure we don't exceed FUEL_STATUS array Add a check to ensure we don't go over the FUEL_STATUS array bounds if the vehicle returns an invalid value above 16. Signed-off-by: Alistair Francis --- obd/decoders.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/obd/decoders.py b/obd/decoders.py index 6ef93835..3a700a46 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -299,12 +299,18 @@ def fuel_status(messages): status_2 = "" if bits[0:8].count(True) == 1: - status_1 = FUEL_STATUS[ 7 - bits[0:8].index(True) ] + if 7 - bits[0:8].index(True) < len(FUEL_STATUS): + status_1 = FUEL_STATUS[ 7 - bits[0:8].index(True) ] + else: + logger.debug("Invalid response for fuel status (high bits set)") else: logger.debug("Invalid response for fuel status (multiple/no bits set)") if bits[8:16].count(True) == 1: - status_2 = FUEL_STATUS[ 7 - bits[8:16].index(True) ] + if 7 - bits[8:16].index(True) < len(FUEL_STATUS): + status_2 = FUEL_STATUS[ 7 - bits[8:16].index(True) ] + else: + logger.debug("Invalid response for fuel status (high bits set)") else: logger.debug("Invalid response for fuel status (multiple/no bits set)") From b56882952608092a9028c1ba0be4917e7d16490f Mon Sep 17 00:00:00 2001 From: Jef Neefs Date: Sun, 11 Nov 2018 17:02:24 +0100 Subject: [PATCH 514/569] Add support for ELM_VOLTAGE responding with a 'V' Add support for the ELM device responding with a voltage that has a 'V' to the obd.commands.ELM_VOLTAGE command. This includes adding a cast to lower a possible uppercase 'V'. It then replaces 'v' with an empty string. Before patch: ======Console Proof========= In [32]: res = connection.query(c) [obd.obd] Sending command: ATRV: Voltage detected by OBD-II adapter [obd.elm327] write: 'ATRV\r\n' [obd.elm327] read: b'12.3V\r\r>' [obd.decoders] Failed to parse ELM voltage After patch: In [34]: res.messages[0].frames[0].raw Out[34]: u'12.3V' --- obd/decoders.py | 3 +++ tests/test_decoders.py | 1 + 2 files changed, 4 insertions(+) diff --git a/obd/decoders.py b/obd/decoders.py index 3a700a46..83099dd4 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -234,6 +234,9 @@ def elm_voltage(messages): # doesn't register as a normal OBD response, # so access the raw frame data v = messages[0].frames[0].raw + # Some ELMs provide float V (for example messages[0].frames[0].raw => u'12.3V' + v = v.lower() + v = v.replace('v', '') try: return float(v) * Unit.volt diff --git a/tests/test_decoders.py b/tests/test_decoders.py index a6e24d9f..61bde5da 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -163,6 +163,7 @@ def test_elm_voltage(): # these aren't parsed as standard hex messages, so manufacture our own assert d.elm_voltage([ Message([ Frame("12.875") ]) ]) == 12.875 * Unit.volt assert d.elm_voltage([ Message([ Frame("12") ]) ]) == 12 * Unit.volt + assert d.elm_voltage([ Message([ Frame(u'12.3V') ]) ]) == 12.3 * Unit.volt assert d.elm_voltage([ Message([ Frame("12ABCD") ]) ]) == None def test_status(): From 58a3b5d16e656982df665fbfbfe99286f185e6cb Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Sat, 17 Nov 2018 09:01:01 -0800 Subject: [PATCH 515/569] tests/test_decoders.py: Convert ' to " To ensure consistency change the ' in the test case to ". Signed-off-by: Alistair Francis --- tests/test_decoders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 61bde5da..f87081e9 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -163,7 +163,7 @@ def test_elm_voltage(): # these aren't parsed as standard hex messages, so manufacture our own assert d.elm_voltage([ Message([ Frame("12.875") ]) ]) == 12.875 * Unit.volt assert d.elm_voltage([ Message([ Frame("12") ]) ]) == 12 * Unit.volt - assert d.elm_voltage([ Message([ Frame(u'12.3V') ]) ]) == 12.3 * Unit.volt + assert d.elm_voltage([ Message([ Frame(u"12.3V") ]) ]) == 12.3 * Unit.volt assert d.elm_voltage([ Message([ Frame("12ABCD") ]) ]) == None def test_status(): From ba3c89b2ea9167aac6bcc5c59ca8441392e86d97 Mon Sep 17 00:00:00 2001 From: apecone Date: Sat, 17 Nov 2018 16:11:09 -0500 Subject: [PATCH 516/569] obd/elm327.py: Change ELM327 command terminating characters to \r According to ELM327 and STN11XX specifications, commands should be terminated with carriage returns only. https://www.kds-online.com/Downloads/OBD/OBDLink_STN11xx-ds.pdf https://www.elmelectronics.com/wp-content/uploads/2016/ELM327DS.pdf Therefore, changed the terminating characters to meet spec and resolve inconsistent behavior in chipset reponses. This is a known resolution for auto baudrate selection failures when using STN11XX chips; And, perhaps, many other prior issues. Signed-off-by: Anthony Pecone --- obd/elm327.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 8e7c86ae..1cb4d747 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -289,7 +289,10 @@ def auto_baudrate(self): # The first character might get eaten if the interface was busy, # so write a second one (again so that the lone CR doesn't repeat # the previous command) - self.__port.write(b"\x7F\x7F\r\n") + + # All commands should be terminated with carriage return according + # to ELM327 and STN11XX specifications + self.__port.write(b"\x7F\x7F\r") self.__port.flush() response = self.__port.read(1024) logger.debug("Response from baud %d: %s" % (baud, repr(response))) @@ -415,7 +418,7 @@ def __write(self, cmd): """ if self.__port: - cmd += b"\r\n" # terminate + cmd += b"\r" # terminate with carriage return in accordance with ELM327 and STN11XX specifications logger.debug("write: " + repr(cmd)) self.__port.flushInput() # dump everything in the input buffer self.__port.write(cmd) # turn the string into bytes and write From 31a035cef26a19799161b4e8c84d5b8d1632b49d Mon Sep 17 00:00:00 2001 From: Ircama Date: Sat, 1 Dec 2018 09:45:20 +0100 Subject: [PATCH 517/569] Catch 'UNABLE TO CONNECT' when first querying protocol 0100 --- obd/elm327.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/obd/elm327.py b/obd/elm327.py index 1cb4d747..59c35a32 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -220,6 +220,9 @@ def auto_protocol(self): # -------------- 0100 (first command, SEARCH protocols) -------------- r0100 = self.__send(b"0100") + if self.__has_message(r0100, "UNABLE TO CONNECT"): + logger.error("Failed to query protocol 0100: unable to connect") + return False # ------------------- ATDPN (list protocol number) ------------------- r = self.__send(b"ATDPN") From fad0702f9eba93f471d42f5edf000f4668827cb5 Mon Sep 17 00:00:00 2001 From: Ircama Date: Thu, 22 Nov 2018 23:22:32 +0100 Subject: [PATCH 518/569] Add support of custom headers Allow usage of the optional `header` argument, which tells python-OBD to use a custom header when querying the command. If not set, python-OBD assumes that the default 7E0 header is needed for querying the command. The switch between default and custom header (and vice versa) is automatically done by python-OBD. Also restore the default header when closing the connection. --- docs/Custom Commands.md | 9 ++++++++- obd/OBDCommand.py | 6 ++++-- obd/obd.py | 19 ++++++++++++++++++- obd/protocols/__init__.py | 2 +- obd/protocols/protocol.py | 5 +++++ 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/docs/Custom Commands.md b/docs/Custom Commands.md index c5f2c68a..0de504c7 100644 --- a/docs/Custom Commands.md +++ b/docs/Custom Commands.md @@ -10,6 +10,7 @@ If the command you need is not in python-OBDs tables, you can create a new `OBDC | decoder | callable | Function used for decoding messages from the OBD adapter | | ecu (optional) | ECU | ID of the ECU this command should listen to (`ECU.ALL` by default) | | fast (optional) | bool | Allows python-OBD to alter this command for efficieny (`False` by default) | +| header (optional) | string | If set, use a custom header instead of the defalut one (7E0) | Example @@ -95,7 +96,13 @@ The `ecu` argument is a constant used to filter incoming messages. Some commands ### OBDCommand.fast -The `fast` argument tells python-OBD whether it is safe to append a `"01"` to the end of the command. This will instruct the adapter to return the first response it recieves, rather than waiting for more (and eventually reaching a timeout). This can speed up requests significantly, and is enabled for most of python-OBDs internal commands. However, for unusual commands, it is safest to leave this disabled. +The optional `fast` argument tells python-OBD whether it is safe to append a `"01"` to the end of the command. This will instruct the adapter to return the first response it recieves, rather than waiting for more (and eventually reaching a timeout). This can speed up requests significantly, and is enabled for most of python-OBDs internal commands. However, for unusual commands, it is safest to leave this disabled. + +--- + +### OBDCommand.header + +The optional `header` argument tells python-OBD to use a custom header when querying the command. If not set, python-OBD assumes that the default 7E0 header is needed for querying the command. The switch between default and custom header (and vice versa) is automatically done by python-OBD. --- diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 5b5b9735..2ac95940 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -31,7 +31,7 @@ ######################################################################## from .utils import * -from .protocols import ECU +from .protocols import ECU, ECU_HEADER from .OBDResponse import OBDResponse import logging @@ -47,7 +47,8 @@ def __init__(self, _bytes, decoder, ecu=ECU.ALL, - fast=False): + fast=False, + header=ECU_HEADER.ENGINE): self.name = name # human readable name (also used as key in commands dict) self.desc = desc # human readable description self.command = command # command string @@ -55,6 +56,7 @@ def __init__(self, self.decode = decoder # decoding function self.ecu = ecu # ECU ID from which this command expects messages from self.fast = fast # can an extra digit be added to the end of the command? (to make the ELM return early) + self.header = header # ECU header used for the queries def clone(self): return OBDCommand(self.name, diff --git a/obd/obd.py b/obd/obd.py index 84dd334f..82d9a614 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -38,6 +38,7 @@ from .commands import commands from .OBDResponse import OBDResponse from .utils import scan_serial, OBDStatus +from .protocols import ECU_HEADER logger = logging.getLogger(__name__) @@ -55,6 +56,7 @@ def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True, self.fast = fast # global switch for disabling optimizations self.timeout = timeout self.__last_command = b"" # used for running the previous command with a CR + self.__last_header = ECU_HEADER.ENGINE # for comparing with the previously used header self.__frame_counts = {} # keeps track of the number of return frames for each command logger.info("======================= python-OBD (v%s) =======================" % __version__) @@ -138,6 +140,19 @@ def __load_commands(self): logger.info("finished querying with %d commands supported" % len(self.supported_commands)) + def __set_header(self, header): + if header == self.__last_header: + return + r = self.interface.send_and_parse(b'AT SH ' + header + b' ') + if not r: + logger.info("Set Header ('AT SH %s') did not return data", header) + return OBDResponse() + if "\n".join([ m.raw() for m in r ]) != "OK": + logger.info("Set Header ('AT SH %s') did not return 'OK'", header) + return OBDResponse() + self.__last_header = header + + def close(self): """ Closes the connection, and clears supported_commands @@ -147,6 +162,7 @@ def close(self): if self.interface is not None: logger.info("Closing connection") + self.__set_header(ECU_HEADER.ENGINE) self.interface.close() self.interface = None @@ -254,7 +270,8 @@ def query(self, cmd, force=False): if not force and not self.test_cmd(cmd): return OBDResponse() - # send command and retrieve message + self.__set_header(cmd.header) + logger.info("Sending command: %s" % str(cmd)) cmd_string = self.__build_command_string(cmd) messages = self.interface.send_and_parse(cmd_string) diff --git a/obd/protocols/__init__.py b/obd/protocols/__init__.py index bfec7dee..a91069fd 100644 --- a/obd/protocols/__init__.py +++ b/obd/protocols/__init__.py @@ -30,7 +30,7 @@ # # ######################################################################## -from .protocol import ECU +from .protocol import ECU, ECU_HEADER from .protocol_unknown import UnknownProtocol diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 62d9f825..93dd06cb 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -45,6 +45,11 @@ """ +class ECU_HEADER: + """ Values for the ECU headers """ + ENGINE = b'7E0' + + class ECU: """ constant flags used for marking and filtering messages """ From d454521c3eca7fc98a2429d6e87bbf29ef12a582 Mon Sep 17 00:00:00 2001 From: Ircama Date: Mon, 3 Dec 2018 08:59:55 +0100 Subject: [PATCH 519/569] check_voltage argument on OBD and Async Added check_voltage optional argument that is `True` by default and when set to `False` disables the detection of the car supply voltage on OBDII port (which should be about 12V). This control assumes that, if the voltage is lower than 6V, the OBDII port is disconnected from the car. If the option is enabled, it adds the `OBDStatus.OBD_CONNECTED` status, which is set when enough voltage is returned (socket connected to the car) but the ignition is off (no communication with the vehicle). Setting the option to `False` should be needed when the adapter does not support the voltage pin or more generally when the hardware provides unreliable results, or if the pin reads the switched ignition voltage rather than the battery positive (this depends on the car). --- docs/Connections.md | 20 ++++++++++++++++---- obd/asynchronous.py | 4 ++-- obd/elm327.py | 25 +++++++++++++++++++++++-- obd/obd.py | 11 ++++++----- obd/utils.py | 1 + 5 files changed, 48 insertions(+), 13 deletions(-) diff --git a/docs/Connections.md b/docs/Connections.md index 7d85c1e2..d30489a3 100644 --- a/docs/Connections.md +++ b/docs/Connections.md @@ -20,7 +20,7 @@ connection = obd.OBD(ports[0]) # connect to the first port in the list
-### OBD(portstr=None, baudrate=None, protocol=None, fast=True, timeout=0.1): +### OBD(portstr=None, baudrate=None, protocol=None, fast=True, timeout=0.1, check_voltage=True): `portstr`: The UNIX device file or Windows COM Port for your adapter. The default value (`None`) will auto select a port. @@ -37,6 +37,8 @@ Disabling fast mode will guarantee that python-OBD outputs the unaltered command `timeout`: Specifies the connection timeout in seconds. +`check_voltage`: Optional argument that is `True` by default and when set to `False` disables the detection of the car supply voltage on OBDII port (which should be about 12V). This control assumes that, if the voltage is lower than 6V, the OBDII port is disconnected from the car. If the option is enabled, it adds the `OBDStatus.OBD_CONNECTED` status, which is set when enough voltage is returned (socket connected to the car) but the ignition is off (no communication with the vehicle). Setting the option to `False` should be needed when the adapter does not support the voltage pin or more generally when the hardware provides unreliable results, or if the pin reads the switched ignition voltage rather than the battery positive (this depends on the car). +
--- @@ -58,7 +60,7 @@ r = connection.query(obd.commands.RPM) # returns the response from the car ### status() -Returns a string value reflecting the status of the connection. These values should be compared against the `OBDStatus` class. The fact that they are strings is for human readability only. There are currently 3 possible states: +Returns a string value reflecting the status of the connection after OBD() or Async() methods are executed. These values should be compared against the `OBDStatus` class. The fact that they are strings is for human readability only. There are currently 4 possible states: ```python from obd import OBDStatus @@ -69,11 +71,21 @@ OBDStatus.NOT_CONNECTED # "Not Connected" # successful communication with the ELM327 adapter OBDStatus.ELM_CONNECTED # "ELM Connected" -# successful communication with the ELM327 and the vehicle +# successful communication with the ELM327 adapter, +# OBD port connected to the car, ignition off +# (not available with argument "check_voltage=False") +OBDStatus.OBD_CONNECTED # "OBD Connected" + +# successful communication with the ELM327 and the +# vehicle; ignition on OBDStatus.CAR_CONNECTED # "Car Connected" ``` -The middle state, `ELM_CONNECTED` is mostly for diagnosing errors. When a proper connection is established, you will never encounter this value. +The status is set by `OBD()` or `Async()` methods and remains unmodified during the connection. `status()` shall not be checked after the queries to verify that the connection is kept active. + +`ELM_CONNECTED` and `OBD_CONNECTED` are mostly for diagnosing errors. When a proper connection is established with the vehicle, you will never encounter these values. + +The ELM327 controller allows OBD Commands and AT Commands. In general, OBD Commands (which interact with the car) can be succesfully performed when the ignition is on, while AT Commands (which generally interact with the ELM327 controller) are always accepted. As the connection phase (for both `OBD` and `Async` objects) also performs OBD protocol commands (after the initial set of AT Commands) and returns the “Car Connected” status (“CAR_CONNECTED”) if the overall connection phase is successful, this status means that the serial communication is valid, that the ELM327 adapter is appropriately responding, that the OBDII socket is connected to the car and also that the ignition is on. “OBD Connected” status (“OBD_CONNECTED”) is returned when the OBDII socket is connected and the ignition is off, while the "ELM Connected" status (“ELM_CONNECTED”) means that the ELM327 processor is reached but the OBDII socket is not connected to the car. “OBD Connected” is controlled by the `check_voltage` option that by default is set to `True` and gets the ignition status when the socket is connected. If the OBDII socket does not support the unswitched battery positive supply, or the OBDII adapter cannot detect it, then the `check_voltage` option should be set to `False`; in such case, the "ELM Connected" status is returned when the socket is not connected or when the ignition is off, with no differentiation. --- diff --git a/obd/asynchronous.py b/obd/asynchronous.py index fc1af5ad..be64c915 100644 --- a/obd/asynchronous.py +++ b/obd/asynchronous.py @@ -46,9 +46,9 @@ class Async(OBD): """ def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True, - timeout=0.1): + timeout=0.1, check_voltage=True): super(Async, self).__init__(portstr, baudrate, protocol, fast, - timeout) + timeout, check_voltage) self.__commands = {} # key = OBDCommand, value = Response self.__callbacks = {} # key = OBDCommand, value = list of Functions self.__thread = None diff --git a/obd/elm327.py b/obd/elm327.py index 59c35a32..04ccaedc 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -103,7 +103,8 @@ class ELM327: - def __init__(self, portname, baudrate, protocol, timeout): + def __init__(self, portname, baudrate, protocol, timeout, + check_voltage=True): """Initializes port by resetting device and gettings supported PIDs. """ logger.info("Initializing ELM327: PORT=%s BAUD=%s PROTOCOL=%s" % @@ -168,6 +169,22 @@ def __init__(self, portname, baudrate, protocol, timeout): # by now, we've successfuly communicated with the ELM, but not the car self.__status = OBDStatus.ELM_CONNECTED + # -------------------------- AT RV (read volt) ------------------------ + if check_voltage: + r = self.__send(b"AT RV") + if not r or len(r) != 1 or r[0] == '': + self.__error("No answer from 'AT RV'") + return + try: + if float(r[0].lower().replace('v', '')) < 6: + logger.error("OBD2 socket disconnected") + return + except ValueError as e: + self.__error("Incorrect response from 'AT RV'") + return + # by now, we've successfuly connected to the OBD socket + self.__status = OBDStatus.OBD_CONNECTED + # try to communicate with the car, and load the correct protocol parser if self.set_protocol(protocol): self.__status = OBDStatus.CAR_CONNECTED @@ -178,7 +195,11 @@ def __init__(self, portname, baudrate, protocol, timeout): self.__protocol.ELM_ID, )) else: - logger.error("Connected to the adapter, but failed to connect to the vehicle") + if self.__status == OBDStatus.OBD_CONNECTED: + logger.error("Adapter connected, but the ignition is off") + else: + logger.error("Connected to the adapter, "\ + "but failed to connect to the vehicle") def set_protocol(self, protocol): diff --git a/obd/obd.py b/obd/obd.py index 82d9a614..e831ae2b 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -50,7 +50,7 @@ class OBD(object): """ def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True, - timeout=0.1): + timeout=0.1, check_voltage=True): self.interface = None self.supported_commands = set(commands.base_commands()) self.fast = fast # global switch for disabling optimizations @@ -60,12 +60,13 @@ def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True, self.__frame_counts = {} # keeps track of the number of return frames for each command logger.info("======================= python-OBD (v%s) =======================" % __version__) - self.__connect(portstr, baudrate, protocol) # initialize by connecting and loading sensors + self.__connect(portstr, baudrate, protocol, + check_voltage) # initialize by connecting and loading sensors self.__load_commands() # try to load the car's supported commands logger.info("===================================================================") - def __connect(self, portstr, baudrate, protocol): + def __connect(self, portstr, baudrate, protocol, check_voltage): """ Attempts to instantiate an ELM327 connection object. """ @@ -82,14 +83,14 @@ def __connect(self, portstr, baudrate, protocol): for port in portnames: logger.info("Attempting to use port: " + str(port)) self.interface = ELM327(port, baudrate, protocol, - self.timeout) + self.timeout, check_voltage) if self.interface.status() >= OBDStatus.ELM_CONNECTED: break # success! stop searching for serial else: logger.info("Explicit port defined") self.interface = ELM327(portstr, baudrate, protocol, - self.timeout) + self.timeout, check_voltage) # if the connection failed, close it if self.interface.status() == OBDStatus.NOT_CONNECTED: diff --git a/obd/utils.py b/obd/utils.py index a5ee9351..679c27ea 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -46,6 +46,7 @@ class OBDStatus: NOT_CONNECTED = "Not Connected" ELM_CONNECTED = "ELM Connected" + OBD_CONNECTED = "OBD Connected" CAR_CONNECTED = "Car Connected" From 684a5e4894c443c614e66240b530493ec16d9026 Mon Sep 17 00:00:00 2001 From: Ircama Date: Sat, 29 Dec 2018 03:11:33 +0100 Subject: [PATCH 520/569] Better Async management Manage connection drop while Async is active without throwing an exception. Add delay_cmds argument to Async. Revised documentation. --- docs/Async Connections.md | 19 ++++++++++++++++++- obd/asynchronous.py | 9 ++++++++- obd/elm327.py | 22 ++++++++++++++++++---- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/docs/Async Connections.md b/docs/Async Connections.md index b3de1eb3..b4074be1 100644 --- a/docs/Async Connections.md +++ b/docs/Async Connections.md @@ -2,10 +2,18 @@ Since the standard `query()` function is blocking, it can be a hazard for UI eve The update loop is controlled by calling `start()` and `stop()`. To subscribe a command for updating, call `watch()` with your requested OBDCommand. Because the update loop is threaded, commands can only be `watch`ed while the loop is `stop`ed. +General sequence to enable an asynchronous connection allowing non-blocking queries: +- *Async()* # set-up the connection (to be used in place of *OBD()*) +- *watch()* # add commands to the watch list +- *start()* # start a thread performing the update loop in background +- *query()* # perform the non-blocking query + +Example: + ```python import obd -connection = obd.Async() # same constructor as 'obd.OBD()' +connection = obd.Async() # same constructor as 'obd.OBD()'; see below. connection.watch(obd.commands.RPM) # keep track of the RPM @@ -39,6 +47,15 @@ connection.stop() --- +### Async(portstr=None, baudrate=None, protocol=None, fast=True, timeout=0.1, check_voltage=True, delay_cmds=0.25) + +Create asynchronous connection. +Arguments are the same as 'obd.OBD()' with the addition of *delay_cmds*, which defaults to 0.25 seconds and allows +controlling a delay after each loop executing all *watch*ed commands in background. If *delay_cmds* is set to 0, +the background thread continuously repeats the execution of all commands without any delay. + +--- + ### start() Starts the update loop. diff --git a/obd/asynchronous.py b/obd/asynchronous.py index be64c915..c50bd6a6 100644 --- a/obd/asynchronous.py +++ b/obd/asynchronous.py @@ -46,7 +46,7 @@ class Async(OBD): """ def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True, - timeout=0.1, check_voltage=True): + timeout=0.1, check_voltage=True, delay_cmds=0.25): super(Async, self).__init__(portstr, baudrate, protocol, fast, timeout, check_voltage) self.__commands = {} # key = OBDCommand, value = Response @@ -54,6 +54,7 @@ def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True, self.__thread = None self.__running = False self.__was_running = False # used with __enter__() and __exit__() + self.__delay_cmds = delay_cmds @property @@ -215,6 +216,11 @@ def run(self): if len(self.__commands) > 0: # loop over the requested commands, send, and collect the response for c in self.__commands: + if not self.is_connected(): + logger.info("Async thread terminated because device disconnected") + self.__running = False + self.__thread = None + return # force, since commands are checked for support in watch() r = super(Async, self).query(c, force=True) @@ -225,6 +231,7 @@ def run(self): # fire the callbacks, if there are any for callback in self.__callbacks[c]: callback(r) + time.sleep(self.__delay_cmds) else: time.sleep(0.25) # idle diff --git a/obd/elm327.py b/obd/elm327.py index 04ccaedc..d8b6b17e 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -444,9 +444,16 @@ def __write(self, cmd): if self.__port: cmd += b"\r" # terminate with carriage return in accordance with ELM327 and STN11XX specifications logger.debug("write: " + repr(cmd)) - self.__port.flushInput() # dump everything in the input buffer - self.__port.write(cmd) # turn the string into bytes and write - self.__port.flush() # wait for the output buffer to finish transmitting + try: + self.__port.flushInput() # dump everything in the input buffer + self.__port.write(cmd) # turn the string into bytes and write + self.__port.flush() # wait for the output buffer to finish transmitting + except Exception: + self.__status = OBDStatus.NOT_CONNECTED + self.__port.close() + self.__port = None + logger.critical("Device disconnected while writing") + return else: logger.info("cannot perform __write() when unconnected") @@ -466,7 +473,14 @@ def __read(self): while True: # retrieve as much data as possible - data = self.__port.read(self.__port.in_waiting or 1) + try: + data = self.__port.read(self.__port.in_waiting or 1) + except Exception: + self.__status = OBDStatus.NOT_CONNECTED + self.__port.close() + self.__port = None + logger.critical("Device disconnected while reading") + return [] # if nothing was recieved if not data: From bd8e64400a783ecdebf851ee77232c7adb6f48a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kaczmarczyk?= Date: Wed, 27 Feb 2019 22:28:24 -0800 Subject: [PATCH 521/569] docs: Fix incorrect whitespace according to PEP8 rules Signed-off-by: Alistair Francis --- docs/Command Tables.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/Command Tables.md b/docs/Command Tables.md index f48ffa1a..8ea0046f 100644 --- a/docs/Command Tables.md +++ b/docs/Command Tables.md @@ -11,7 +11,7 @@ |PID | Name | Description | Response Value | |----|---------------------------|-----------------------------------------|-----------------------| -| 00 | PIDS_A | Supported PIDs [01-20] | bitarray | +| 00 | PIDS_A | Supported PIDs [01-20] | BitArray | | 01 | STATUS | Status since DTCs cleared | [special](Responses.md#status) | | 02 | FREEZE_DTC | DTC that triggered the freeze frame | [special](Responses.md#diagnostic-trouble-codes-dtcs) | | 03 | FUEL_STATUS | Fuel System Status | [(string, string)](Responses.md#fuel-status) | @@ -43,7 +43,7 @@ | 1D | O2_SENSORS_ALT | O2 Sensors Present (alternate) | [special](Responses.md#oxygen-sensors-present) | | 1E | AUX_INPUT_STATUS | Auxiliary input status (power take off) | boolean | | 1F | RUN_TIME | Engine Run Time | Unit.second | -| 20 | PIDS_B | Supported PIDs [21-40] | bitarray | +| 20 | PIDS_B | Supported PIDs [21-40] | BitArray | | 21 | DISTANCE_W_MIL | Distance Traveled with MIL on | Unit.kilometer | | 22 | FUEL_RAIL_PRESSURE_VAC | Fuel Rail Pressure (relative to vacuum) | Unit.kilopascal | | 23 | FUEL_RAIL_PRESSURE_DIRECT | Fuel Rail Pressure (direct inject) | Unit.kilopascal | @@ -75,7 +75,7 @@ | 3D | CATALYST_TEMP_B2S1 | Catalyst Temperature: Bank 2 - Sensor 1 | Unit.celsius | | 3E | CATALYST_TEMP_B1S2 | Catalyst Temperature: Bank 1 - Sensor 2 | Unit.celsius | | 3F | CATALYST_TEMP_B2S2 | Catalyst Temperature: Bank 2 - Sensor 2 | Unit.celsius | -| 40 | PIDS_C | Supported PIDs [41-60] | bitarray | +| 40 | PIDS_C | Supported PIDs [41-60] | BitArray | | 41 | STATUS_DRIVE_CYCLE | Monitor status this drive cycle | [special](Responses.md#status) | | 42 | CONTROL_MODULE_VOLTAGE | Control module voltage | Unit.volt | | 43 | ABSOLUTE_LOAD | Absolute load value | Unit.percent | @@ -151,7 +151,7 @@ Mode 06 commands are used to monitor various test results from the vehicle. All |PID | Name | Description | Response Value | |-------|-----------------------------|--------------------------------------------|-----------------------| -| 00 | MIDS_A | Supported MIDs [01-20] | bitarray | +| 00 | MIDS_A | Supported MIDs [01-20] | BitArray | | 01 | MONITOR_O2_B1S1 | O2 Sensor Monitor Bank 1 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) | | 02 | MONITOR_O2_B1S2 | O2 Sensor Monitor Bank 1 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) | | 03 | MONITOR_O2_B1S3 | O2 Sensor Monitor Bank 1 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) | @@ -169,7 +169,7 @@ Mode 06 commands are used to monitor various test results from the vehicle. All | 0F | MONITOR_O2_B4S3 | O2 Sensor Monitor Bank 4 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) | | 10 | MONITOR_O2_B4S4 | O2 Sensor Monitor Bank 4 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| 20 | MIDS_B | Supported MIDs [21-40] | bitarray | +| 20 | MIDS_B | Supported MIDs [21-40] | BitArray | | 21 | MONITOR_CATALYST_B1 | Catalyst Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | | 22 | MONITOR_CATALYST_B2 | Catalyst Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) | | 23 | MONITOR_CATALYST_B3 | Catalyst Monitor Bank 3 | [monitor](Responses.md#monitors-mode-06-responses) | @@ -189,7 +189,7 @@ Mode 06 commands are used to monitor various test results from the vehicle. All | 3C | MONITOR_EVAP_020 | EVAP Monitor (0.020\") | [monitor](Responses.md#monitors-mode-06-responses) | | 3D | MONITOR_PURGE_FLOW | Purge Flow Monitor | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| 40 | MIDS_C | Supported MIDs [41-60] | bitarray | +| 40 | MIDS_C | Supported MIDs [41-60] | BitArray | | 41 | MONITOR_O2_HEATER_B1S1 | O2 Sensor Heater Monitor Bank 1 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) | | 42 | MONITOR_O2_HEATER_B1S2 | O2 Sensor Heater Monitor Bank 1 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) | | 43 | MONITOR_O2_HEATER_B1S3 | O2 Sensor Heater Monitor Bank 1 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) | @@ -207,7 +207,7 @@ Mode 06 commands are used to monitor various test results from the vehicle. All | 4F | MONITOR_O2_HEATER_B4S3 | O2 Sensor Heater Monitor Bank 4 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) | | 50 | MONITOR_O2_HEATER_B4S4 | O2 Sensor Heater Monitor Bank 4 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| 60 | MIDS_D | Supported MIDs [61-80] | bitarray | +| 60 | MIDS_D | Supported MIDs [61-80] | BitArray | | 61 | MONITOR_HEATED_CATALYST_B1 | Heated Catalyst Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | | 62 | MONITOR_HEATED_CATALYST_B2 | Heated Catalyst Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) | | 63 | MONITOR_HEATED_CATALYST_B3 | Heated Catalyst Monitor Bank 3 | [monitor](Responses.md#monitors-mode-06-responses) | @@ -218,7 +218,7 @@ Mode 06 commands are used to monitor various test results from the vehicle. All | 73 | MONITOR_SECONDARY_AIR_3 | Secondary Air Monitor 3 | [monitor](Responses.md#monitors-mode-06-responses) | | 74 | MONITOR_SECONDARY_AIR_4 | Secondary Air Monitor 4 | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| 80 | MIDS_E | Supported MIDs [81-A0] | bitarray | +| 80 | MIDS_E | Supported MIDs [81-A0] | BitArray | | 81 | MONITOR_FUEL_SYSTEM_B1 | Fuel System Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | | 82 | MONITOR_FUEL_SYSTEM_B2 | Fuel System Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) | | 83 | MONITOR_FUEL_SYSTEM_B3 | Fuel System Monitor Bank 3 | [monitor](Responses.md#monitors-mode-06-responses) | @@ -232,7 +232,7 @@ Mode 06 commands are used to monitor various test results from the vehicle. All | 98 | MONITOR_NOX_CATALYST_B1 | NOx Catalyst Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) | | 99 | MONITOR_NOX_CATALYST_B2 | NOx Catalyst Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) | | *gap* | | | -| A0 | MIDS_F | Supported MIDs [A1-C0] | bitarray | +| A0 | MIDS_F | Supported MIDs [A1-C0] | BitArray | | A1 | MONITOR_MISFIRE_GENERAL | Misfire Monitor General Data | [monitor](Responses.md#monitors-mode-06-responses) | | A2 | MONITOR_MISFIRE_CYLINDER_1 | Misfire Cylinder 1 Data | [monitor](Responses.md#monitors-mode-06-responses) | | A3 | MONITOR_MISFIRE_CYLINDER_2 | Misfire Cylinder 2 Data | [monitor](Responses.md#monitors-mode-06-responses) | From 596cc70bddc6d4f3d12b3ea625317d534b295803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kaczmarczyk?= Date: Wed, 27 Feb 2019 22:31:28 -0800 Subject: [PATCH 522/569] obd: Fix some incorrect whitespace according to PEP8 rules Signed-off-by: Alistair Francis --- obd/OBDCommand.py | 36 ++++---- obd/OBDResponse.py | 40 +++++---- obd/UnitsAndScaling.py | 191 ++++++++++++++++++++--------------------- obd/__init__.py | 2 +- obd/__version__.py | 1 - obd/asynchronous.py | 38 +++----- 6 files changed, 146 insertions(+), 162 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 2ac95940..4b52b59e 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -39,7 +39,7 @@ logger = logging.getLogger(__name__) -class OBDCommand(): +class OBDCommand: def __init__(self, name, desc, @@ -49,14 +49,14 @@ def __init__(self, ecu=ECU.ALL, fast=False, header=ECU_HEADER.ENGINE): - self.name = name # human readable name (also used as key in commands dict) - self.desc = desc # human readable description - self.command = command # command string - self.bytes = _bytes # number of bytes expected in return - self.decode = decoder # decoding function - self.ecu = ecu # ECU ID from which this command expects messages from - self.fast = fast # can an extra digit be added to the end of the command? (to make the ELM return early) - self.header = header # ECU header used for the queries + self.name = name # human readable name (also used as key in commands dict) + self.desc = desc # human readable description + self.command = command # command string + self.bytes = _bytes # number of bytes expected in return + self.decode = decoder # decoding function + self.ecu = ecu # ECU ID from which this command expects messages from + self.fast = fast # can an extra digit be added to the end of the command? (to make the ELM return early) + self.header = header # ECU header used for the queries def clone(self): return OBDCommand(self.name, @@ -69,42 +69,37 @@ def clone(self): @property def mode(self): - if len(self.command) >= 2 and \ - isHex(self.command.decode()): + if len(self.command) >= 2 and isHex(self.command.decode()): return int(self.command[:2], 16) else: return None @property def pid(self): - if len(self.command) > 2 and \ - isHex(self.command.decode()): + if len(self.command) > 2 and isHex(self.command.decode()): return int(self.command[2:], 16) else: return None - def __call__(self, messages): # filter for applicable messages (from the right ECU(s)) - for_us = lambda m: (self.ecu & m.ecu) > 0 - messages = list(filter(for_us, messages)) + messages = [m for m in messages if (self.ecu & m.ecu) > 0] # guarantee data size for the decoder for m in messages: self.__constrain_message_data(m) - # create the response object with the raw data recieved + # create the response object with the raw data received # and reference to original command r = OBDResponse(self, messages) if messages: r.value = self.decode(messages) else: - logger.info(str(self) + " did not recieve any acceptable messages") + logger.info(str(self) + " did not receive any acceptable messages") return r - def __constrain_message_data(self, message): """ pads or chops the data field to the size specified by this command """ if self.bytes > 0: @@ -117,7 +112,6 @@ def __constrain_message_data(self, message): message.data += (b'\x00' * (self.bytes - len(message.data))) logger.debug("Message was shorter than expected. Padded message: " + repr(message.data)) - def __str__(self): return "%s: %s" % (self.command, self.desc) @@ -127,6 +121,6 @@ def __hash__(self): def __eq__(self, other): if isinstance(other, OBDCommand): - return (self.command == other.command) + return self.command == other.command else: return False diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py index 4d697eaf..e72c7e4c 100644 --- a/obd/OBDResponse.py +++ b/obd/OBDResponse.py @@ -31,30 +31,36 @@ ######################################################################## +import logging +import sys import time -from .codes import * -import logging +from .codes import * logger = logging.getLogger(__name__) +if sys.version[0] < '3': + string_types = (str, unicode) +else: + string_types = (str,) -class OBDResponse(): +class OBDResponse: """ Standard response object for any OBDCommand """ def __init__(self, command=None, messages=None): - self.command = command + self.command = command self.messages = messages if messages else [] - self.value = None - self.time = time.time() + self.value = None + self.time = time.time() @property def unit(self): # for backwards compatibility + from obd import Unit # local import to avoid cyclic-dependency if isinstance(self.value, Unit.Quantity): return str(self.value.u) - elif self.value == None: + elif self.value is None: return None else: return str(type(self.value)) @@ -66,17 +72,16 @@ def __str__(self): return str(self.value) - """ Special value types used in OBDResponses instantiated in decoders.py """ -class Status(): +class Status: def __init__(self): - self.MIL = False - self.DTC_count = 0 + self.MIL = False + self.DTC_count = 0 self.ignition_type = "" # make sure each test is available by name @@ -84,7 +89,7 @@ def __init__(self): # breaking when the user looks up a standard test that's null. null_test = StatusTest() for name in BASE_TESTS + SPARK_TESTS + COMPRESSION_TESTS: - if name: # filter out None/reserved tests + if name: # filter out None/reserved tests self.__dict__[name] = null_test @@ -100,9 +105,9 @@ def __str__(self): return "Test %s: %s, %s" % (self.name, a, c) -class Monitor(): +class Monitor: def __init__(self): - self._tests = {} # tid : MonitorTest + self._tests = {} # tid : MonitorTest # make the standard TIDs available as null monitor tests # until real data comes it. This also prevents things from @@ -125,7 +130,7 @@ def tests(self): def __str__(self): if len(self.tests) > 0: - return "\n".join([ str(t) for t in self.tests ]) + return "\n".join([str(t) for t in self.tests]) else: return "No tests to report" @@ -135,14 +140,13 @@ def __len__(self): def __getitem__(self, key): if isinstance(key, int): return self._tests.get(key, MonitorTest()) - elif isinstance(key, str) or isinstance(key, unicode): + elif isinstance(key, string_types): return self.__dict__.get(key, MonitorTest()) else: logger.warning("Monitor test results can only be retrieved by TID value or property name") - -class MonitorTest(): +class MonitorTest: def __init__(self): self.tid = None self.name = None diff --git a/obd/UnitsAndScaling.py b/obd/UnitsAndScaling.py index 0d58de39..b49650b1 100644 --- a/obd/UnitsAndScaling.py +++ b/obd/UnitsAndScaling.py @@ -31,8 +31,8 @@ ######################################################################## import pint -from .utils import * +from .utils import * # export the unit registry Unit = pint.UnitRegistry() @@ -43,14 +43,13 @@ Unit.define("ppm = count / 1000000 = PPM = parts_per_million") - -class UAS(): +class UAS: """ Class for representing a Unit and Scale conversion Used in the decoding of Mode 06 monitor responses """ - def __init__(self, signed, scale, unit, offset=0): + def __init__(self, signed, scale, unit, offset=0.0): self.signed = signed self.scale = scale self.unit = unit @@ -70,106 +69,106 @@ def __call__(self, _bytes): # dict for looking up standardized UAS IDs with conversion objects UAS_IDS = { # unsigned ----------------------------------------- - 0x01 : UAS(False, 1, Unit.count), - 0x02 : UAS(False, 0.1, Unit.count), - 0x03 : UAS(False, 0.01, Unit.count), - 0x04 : UAS(False, 0.001, Unit.count), - 0x05 : UAS(False, 0.0000305, Unit.count), - 0x06 : UAS(False, 0.000305, Unit.count), - 0x07 : UAS(False, 0.25, Unit.rpm), - 0x08 : UAS(False, 0.01, Unit.kph), - 0x09 : UAS(False, 1, Unit.kph), - 0x0A : UAS(False, 0.122, Unit.millivolt), - 0x0B : UAS(False, 0.001, Unit.volt), - 0x0C : UAS(False, 0.01, Unit.volt), - 0x0D : UAS(False, 0.00390625, Unit.milliampere), - 0x0E : UAS(False, 0.001, Unit.ampere), - 0x0F : UAS(False, 0.01, Unit.ampere), - 0x10 : UAS(False, 1, Unit.millisecond), - 0x11 : UAS(False, 100, Unit.millisecond), - 0x12 : UAS(False, 1, Unit.second), - 0x13 : UAS(False, 1, Unit.milliohm), - 0x14 : UAS(False, 1, Unit.ohm), - 0x15 : UAS(False, 1, Unit.kiloohm), - 0x16 : UAS(False, 0.1, Unit.celsius, offset=-40.0), - 0x17 : UAS(False, 0.01, Unit.kilopascal), - 0x18 : UAS(False, 0.0117, Unit.kilopascal), - 0x19 : UAS(False, 0.079, Unit.kilopascal), - 0x1A : UAS(False, 1, Unit.kilopascal), - 0x1B : UAS(False, 10, Unit.kilopascal), - 0x1C : UAS(False, 0.01, Unit.degree), - 0x1D : UAS(False, 0.5, Unit.degree), - 0x1E : UAS(False, 0.0000305, Unit.ratio), - 0x1F : UAS(False, 0.05, Unit.ratio), - 0x20 : UAS(False, 0.00390625, Unit.ratio), - 0x21 : UAS(False, 1, Unit.millihertz), - 0x22 : UAS(False, 1, Unit.hertz), - 0x23 : UAS(False, 1, Unit.kilohertz), - 0x24 : UAS(False, 1, Unit.count), - 0x25 : UAS(False, 1, Unit.kilometer), - 0x26 : UAS(False, 0.1, Unit.millivolt / Unit.millisecond), - 0x27 : UAS(False, 0.01, Unit.grams_per_second), - 0x28 : UAS(False, 1, Unit.grams_per_second), - 0x29 : UAS(False, 0.25, Unit.pascal / Unit.second), - 0x2A : UAS(False, 0.001, Unit.kilogram / Unit.hour), - 0x2B : UAS(False, 1, Unit.count), - 0x2C : UAS(False, 0.01, Unit.gram), # per-cylinder - 0x2D : UAS(False, 0.01, Unit.milligram), # per-stroke - 0x2E : lambda _bytes: any([ bool(x) for x in _bytes]), - 0x2F : UAS(False, 0.01, Unit.percent), - 0x30 : UAS(False, 0.001526, Unit.percent), - 0x31 : UAS(False, 0.001, Unit.liter), - 0x32 : UAS(False, 0.0000305, Unit.inch), - 0x33 : UAS(False, 0.00024414, Unit.ratio), - 0x34 : UAS(False, 1, Unit.minute), - 0x35 : UAS(False, 10, Unit.millisecond), - 0x36 : UAS(False, 0.01, Unit.gram), - 0x37 : UAS(False, 0.1, Unit.gram), - 0x38 : UAS(False, 1, Unit.gram), - 0x39 : UAS(False, 0.01, Unit.percent, offset=-327.68), - 0x3A : UAS(False, 0.001, Unit.gram), - 0x3B : UAS(False, 0.0001, Unit.gram), - 0x3C : UAS(False, 0.1, Unit.microsecond), - 0x3D : UAS(False, 0.01, Unit.milliampere), - 0x3E : UAS(False, 0.00006103516, Unit.millimeter ** 2), - 0x3F : UAS(False, 0.01, Unit.liter), - 0x40 : UAS(False, 1, Unit.ppm), - 0x41 : UAS(False, 0.01, Unit.microampere), + 0x01: UAS(False, 1, Unit.count), + 0x02: UAS(False, 0.1, Unit.count), + 0x03: UAS(False, 0.01, Unit.count), + 0x04: UAS(False, 0.001, Unit.count), + 0x05: UAS(False, 0.0000305, Unit.count), + 0x06: UAS(False, 0.000305, Unit.count), + 0x07: UAS(False, 0.25, Unit.rpm), + 0x08: UAS(False, 0.01, Unit.kph), + 0x09: UAS(False, 1, Unit.kph), + 0x0A: UAS(False, 0.122, Unit.millivolt), + 0x0B: UAS(False, 0.001, Unit.volt), + 0x0C: UAS(False, 0.01, Unit.volt), + 0x0D: UAS(False, 0.00390625, Unit.milliampere), + 0x0E: UAS(False, 0.001, Unit.ampere), + 0x0F: UAS(False, 0.01, Unit.ampere), + 0x10: UAS(False, 1, Unit.millisecond), + 0x11: UAS(False, 100, Unit.millisecond), + 0x12: UAS(False, 1, Unit.second), + 0x13: UAS(False, 1, Unit.milliohm), + 0x14: UAS(False, 1, Unit.ohm), + 0x15: UAS(False, 1, Unit.kiloohm), + 0x16: UAS(False, 0.1, Unit.celsius, offset=-40.0), + 0x17: UAS(False, 0.01, Unit.kilopascal), + 0x18: UAS(False, 0.0117, Unit.kilopascal), + 0x19: UAS(False, 0.079, Unit.kilopascal), + 0x1A: UAS(False, 1, Unit.kilopascal), + 0x1B: UAS(False, 10, Unit.kilopascal), + 0x1C: UAS(False, 0.01, Unit.degree), + 0x1D: UAS(False, 0.5, Unit.degree), + 0x1E: UAS(False, 0.0000305, Unit.ratio), + 0x1F: UAS(False, 0.05, Unit.ratio), + 0x20: UAS(False, 0.00390625, Unit.ratio), + 0x21: UAS(False, 1, Unit.millihertz), + 0x22: UAS(False, 1, Unit.hertz), + 0x23: UAS(False, 1, Unit.kilohertz), + 0x24: UAS(False, 1, Unit.count), + 0x25: UAS(False, 1, Unit.kilometer), + 0x26: UAS(False, 0.1, Unit.millivolt / Unit.millisecond), + 0x27: UAS(False, 0.01, Unit.grams_per_second), + 0x28: UAS(False, 1, Unit.grams_per_second), + 0x29: UAS(False, 0.25, Unit.pascal / Unit.second), + 0x2A: UAS(False, 0.001, Unit.kilogram / Unit.hour), + 0x2B: UAS(False, 1, Unit.count), + 0x2C: UAS(False, 0.01, Unit.gram), # per-cylinder + 0x2D: UAS(False, 0.01, Unit.milligram), # per-stroke + 0x2E: lambda _bytes: any([bool(x) for x in _bytes]), + 0x2F: UAS(False, 0.01, Unit.percent), + 0x30: UAS(False, 0.001526, Unit.percent), + 0x31: UAS(False, 0.001, Unit.liter), + 0x32: UAS(False, 0.0000305, Unit.inch), + 0x33: UAS(False, 0.00024414, Unit.ratio), + 0x34: UAS(False, 1, Unit.minute), + 0x35: UAS(False, 10, Unit.millisecond), + 0x36: UAS(False, 0.01, Unit.gram), + 0x37: UAS(False, 0.1, Unit.gram), + 0x38: UAS(False, 1, Unit.gram), + 0x39: UAS(False, 0.01, Unit.percent, offset=-327.68), + 0x3A: UAS(False, 0.001, Unit.gram), + 0x3B: UAS(False, 0.0001, Unit.gram), + 0x3C: UAS(False, 0.1, Unit.microsecond), + 0x3D: UAS(False, 0.01, Unit.milliampere), + 0x3E: UAS(False, 0.00006103516, Unit.millimeter ** 2), + 0x3F: UAS(False, 0.01, Unit.liter), + 0x40: UAS(False, 1, Unit.ppm), + 0x41: UAS(False, 0.01, Unit.microampere), # signed ----------------------------------------- - 0x81 : UAS(True, 1, Unit.count), - 0x82 : UAS(True, 0.1, Unit.count), - 0x83 : UAS(True, 0.01, Unit.count), - 0x84 : UAS(True, 0.001, Unit.count), - 0x85 : UAS(True, 0.0000305, Unit.count), - 0x86 : UAS(True, 0.000305, Unit.count), - 0x87 : UAS(True, 1, Unit.ppm), + 0x81: UAS(True, 1, Unit.count), + 0x82: UAS(True, 0.1, Unit.count), + 0x83: UAS(True, 0.01, Unit.count), + 0x84: UAS(True, 0.001, Unit.count), + 0x85: UAS(True, 0.0000305, Unit.count), + 0x86: UAS(True, 0.000305, Unit.count), + 0x87: UAS(True, 1, Unit.ppm), # - 0x8A : UAS(True, 0.122, Unit.millivolt), - 0x8B : UAS(True, 0.001, Unit.volt), - 0x8C : UAS(True, 0.01, Unit.volt), - 0x8D : UAS(True, 0.00390625, Unit.milliampere), - 0x8E : UAS(True, 0.001, Unit.ampere), + 0x8A: UAS(True, 0.122, Unit.millivolt), + 0x8B: UAS(True, 0.001, Unit.volt), + 0x8C: UAS(True, 0.01, Unit.volt), + 0x8D: UAS(True, 0.00390625, Unit.milliampere), + 0x8E: UAS(True, 0.001, Unit.ampere), # - 0x90 : UAS(True, 1, Unit.millisecond), + 0x90: UAS(True, 1, Unit.millisecond), # - 0x96 : UAS(True, 0.1, Unit.celsius), + 0x96: UAS(True, 0.1, Unit.celsius), # - 0x99 : UAS(True, 0.1, Unit.kilopascal), + 0x99: UAS(True, 0.1, Unit.kilopascal), # - 0x9C : UAS(True, 0.01, Unit.degree), - 0x9D : UAS(True, 0.5, Unit.degree), + 0x9C: UAS(True, 0.01, Unit.degree), + 0x9D: UAS(True, 0.5, Unit.degree), # - 0xA8 : UAS(True, 1, Unit.grams_per_second), - 0xA9 : UAS(True, 0.25, Unit.pascal / Unit.second), + 0xA8: UAS(True, 1, Unit.grams_per_second), + 0xA9: UAS(True, 0.25, Unit.pascal / Unit.second), # - 0xAD : UAS(True, 0.01, Unit.milligram), # per-stroke - 0xAE : UAS(True, 0.1, Unit.milligram), # per-stroke - 0xAF : UAS(True, 0.01, Unit.percent), - 0xB0 : UAS(True, 0.003052, Unit.percent), - 0xB1 : UAS(True, 2, Unit.millivolt / Unit.second), + 0xAD: UAS(True, 0.01, Unit.milligram), # per-stroke + 0xAE: UAS(True, 0.1, Unit.milligram), # per-stroke + 0xAF: UAS(True, 0.01, Unit.percent), + 0xB0: UAS(True, 0.003052, Unit.percent), + 0xB1: UAS(True, 2, Unit.millivolt / Unit.second), # - 0xFC : UAS(True, 0.01, Unit.kilopascal), - 0xFD : UAS(True, 0.001, Unit.kilopascal), - 0xFE : UAS(True, 0.25, Unit.pascal), + 0xFC: UAS(True, 0.01, Unit.kilopascal), + 0xFD: UAS(True, 0.001, Unit.kilopascal), + 0xFE: UAS(True, 0.25, Unit.pascal), } diff --git a/obd/__init__.py b/obd/__init__.py index a013837d..5002708b 100644 --- a/obd/__init__.py +++ b/obd/__init__.py @@ -52,6 +52,6 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.WARNING) -console_handler = logging.StreamHandler() # sends output to stderr +console_handler = logging.StreamHandler() # sends output to stderr console_handler.setFormatter(logging.Formatter("[%(name)s] %(message)s")) logger.addHandler(console_handler) diff --git a/obd/__version__.py b/obd/__version__.py index c2b99956..a71c5c7f 100644 --- a/obd/__version__.py +++ b/obd/__version__.py @@ -1,2 +1 @@ - __version__ = '0.7.0' diff --git a/obd/asynchronous.py b/obd/asynchronous.py index c50bd6a6..d6f55413 100644 --- a/obd/asynchronous.py +++ b/obd/asynchronous.py @@ -49,19 +49,17 @@ def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True, timeout=0.1, check_voltage=True, delay_cmds=0.25): super(Async, self).__init__(portstr, baudrate, protocol, fast, timeout, check_voltage) - self.__commands = {} # key = OBDCommand, value = Response - self.__callbacks = {} # key = OBDCommand, value = list of Functions - self.__thread = None - self.__running = False - self.__was_running = False # used with __enter__() and __exit__() - self.__delay_cmds = delay_cmds - + self.__commands = {} # key = OBDCommand, value = Response + self.__callbacks = {} # key = OBDCommand, value = list of Functions + self.__thread = None + self.__running = False + self.__was_running = False # used with __enter__() and __exit__() + self.__delay_cmds = delay_cmds @property def running(self): return self.__running - def start(self): """ Starts the async update loop """ if not self.is_connected(): @@ -79,7 +77,6 @@ def start(self): self.__thread.daemon = True self.__thread.start() - def stop(self): """ Stops the async update loop """ if self.__thread is not None: @@ -89,7 +86,6 @@ def stop(self): self.__thread = None logger.info("Async thread stopped") - def paused(self): """ A stub function for semantic purposes only @@ -100,7 +96,6 @@ def paused(self): """ return self - def __enter__(self): """ pauses the async loop, @@ -110,7 +105,6 @@ def __enter__(self): self.stop() return self.__was_running - def __exit__(self, exc_type, exc_value, traceback): """ resumes the update loop if it was running @@ -119,15 +113,13 @@ def __exit__(self, exc_type, exc_value, traceback): if not self.__running and self.__was_running: self.start() - return False # don't suppress any exceptions - + return False # don't suppress any exceptions def close(self): """ Closes the connection """ self.stop() super(Async, self).close() - def watch(self, c, callback=None, force=False): """ Subscribes the given command for continuous updating. Once subscribed, @@ -147,15 +139,14 @@ def watch(self, c, callback=None, force=False): # new command being watched, store the command if c not in self.__commands: logger.info("Watching command: %s" % str(c)) - self.__commands[c] = OBDResponse() # give it an initial value - self.__callbacks[c] = [] # create an empty list + self.__commands[c] = OBDResponse() # give it an initial value + self.__callbacks[c] = [] # create an empty list # if a callback was given, push it if hasattr(callback, "__call__") and (callback not in self.__callbacks[c]): logger.info("subscribing callback for command: %s" % str(c)) self.__callbacks[c].append(callback) - def unwatch(self, c, callback=None): """ Unsubscribes a specific command (and optionally, a specific callback) @@ -182,7 +173,6 @@ def unwatch(self, c, callback=None): self.__callbacks.pop(c, None) self.__commands.pop(c, None) - def unwatch_all(self): """ Unsubscribes all commands and callbacks from being updated """ @@ -191,11 +181,10 @@ def unwatch_all(self): logger.warning("Can't unwatch_all() while running, please use stop()") else: logger.info("Unwatching all") - self.__commands = {} + self.__commands = {} self.__callbacks = {} - - def query(self, c): + def query(self, c, force=False): """ Non-blocking query(). Only commands that have been watch()ed will return valid responses @@ -206,11 +195,10 @@ def query(self, c): else: return OBDResponse() - def run(self): """ Daemon thread """ - # loop until the stop signal is recieved + # loop until the stop signal is received while self.__running: if len(self.__commands) > 0: @@ -234,4 +222,4 @@ def run(self): time.sleep(self.__delay_cmds) else: - time.sleep(0.25) # idle + time.sleep(0.25) # idle From e3be50335f7b96e53a02a1c0dbb9f888d9744c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kaczmarczyk?= Date: Wed, 27 Feb 2019 22:31:59 -0800 Subject: [PATCH 523/569] obd: Fix incorrect whitespace according to PEP8 rules Signed-off-by: Alistair Francis --- obd/codes.py | 625 +++++++++++++++--------------- obd/commands.py | 56 +-- obd/decoders.py | 119 +++--- obd/elm327.py | 131 +++---- obd/obd.py | 53 +-- obd/protocols/protocol.py | 59 ++- obd/protocols/protocol_can.py | 46 +-- obd/protocols/protocol_legacy.py | 34 +- obd/protocols/protocol_unknown.py | 5 +- obd/utils.py | 34 +- 10 files changed, 545 insertions(+), 617 deletions(-) diff --git a/obd/codes.py b/obd/codes.py index b8fea917..fdec628a 100644 --- a/obd/codes.py +++ b/obd/codes.py @@ -1799,306 +1799,305 @@ "P3496": "Cylinder 12 Exhaust Valve Control Circuit High", "P3497": "Cylinder Deactivation System", - - "U0001" : "High Speed CAN Communication Bus", - "U0002" : "High Speed CAN Communication Bus (Performance)", - "U0003" : "High Speed CAN Communication Bus (Open)", - "U0004" : "High Speed CAN Communication Bus (Low)", - "U0005" : "High Speed CAN Communication Bus (High)", - "U0006" : "High Speed CAN Communication Bus (Open)", - "U0007" : "High Speed CAN Communication Bus (Low)", - "U0008" : "High Speed CAN Communication Bus (High)", - "U0009" : "High Speed CAN Communication Bus (shorted to Bus)", - "U0010" : "Medium Speed CAN Communication Bus", - "U0011" : "Medium Speed CAN Communication Bus (Performance)", - "U0012" : "Medium Speed CAN Communication Bus (Open)", - "U0013" : "Medium Speed CAN Communication Bus (Low)", - "U0014" : "Medium Speed CAN Communication Bus (High)", - "U0015" : "Medium Speed CAN Communication Bus (Open)", - "U0016" : "Medium Speed CAN Communication Bus (Low)", - "U0017" : "Medium Speed CAN Communication Bus (High)", - "U0018" : "Medium Speed CAN Communication Bus (shorted to Bus)", - "U0019" : "Low Speed CAN Communication Bus", - "U0020" : "Low Speed CAN Communication Bus (Performance)", - "U0021" : "Low Speed CAN Communication Bus (Open)", - "U0022" : "Low Speed CAN Communication Bus (Low)", - "U0023" : "Low Speed CAN Communication Bus (High)", - "U0024" : "Low Speed CAN Communication Bus (Open)", - "U0025" : "Low Speed CAN Communication Bus (Low)", - "U0026" : "Low Speed CAN Communication Bus (High)", - "U0027" : "Low Speed CAN Communication Bus (shorted to Bus)", - "U0028" : "Vehicle Communication Bus A", - "U0029" : "Vehicle Communication Bus A (Performance)", - "U0030" : "Vehicle Communication Bus A (Open)", - "U0031" : "Vehicle Communication Bus A (Low)", - "U0032" : "Vehicle Communication Bus A (High)", - "U0033" : "Vehicle Communication Bus A (Open)", - "U0034" : "Vehicle Communication Bus A (Low)", - "U0035" : "Vehicle Communication Bus A (High)", - "U0036" : "Vehicle Communication Bus A (shorted to Bus A)", - "U0037" : "Vehicle Communication Bus B", - "U0038" : "Vehicle Communication Bus B (Performance)", - "U0039" : "Vehicle Communication Bus B (Open)", - "U0040" : "Vehicle Communication Bus B (Low)", - "U0041" : "Vehicle Communication Bus B (High)", - "U0042" : "Vehicle Communication Bus B (Open)", - "U0043" : "Vehicle Communication Bus B (Low)", - "U0044" : "Vehicle Communication Bus B (High)", - "U0045" : "Vehicle Communication Bus B (shorted to Bus B)", - "U0046" : "Vehicle Communication Bus C", - "U0047" : "Vehicle Communication Bus C (Performance)", - "U0048" : "Vehicle Communication Bus C (Open)", - "U0049" : "Vehicle Communication Bus C (Low)", - "U0050" : "Vehicle Communication Bus C (High)", - "U0051" : "Vehicle Communication Bus C (Open)", - "U0052" : "Vehicle Communication Bus C (Low)", - "U0053" : "Vehicle Communication Bus C (High)", - "U0054" : "Vehicle Communication Bus C (shorted to Bus C)", - "U0055" : "Vehicle Communication Bus D", - "U0056" : "Vehicle Communication Bus D (Performance)", - "U0057" : "Vehicle Communication Bus D (Open)", - "U0058" : "Vehicle Communication Bus D (Low)", - "U0059" : "Vehicle Communication Bus D (High)", - "U0060" : "Vehicle Communication Bus D (Open)", - "U0061" : "Vehicle Communication Bus D (Low)", - "U0062" : "Vehicle Communication Bus D (High)", - "U0063" : "Vehicle Communication Bus D (shorted to Bus D)", - "U0064" : "Vehicle Communication Bus E", - "U0065" : "Vehicle Communication Bus E (Performance)", - "U0066" : "Vehicle Communication Bus E (Open)", - "U0067" : "Vehicle Communication Bus E (Low)", - "U0068" : "Vehicle Communication Bus E (High)", - "U0069" : "Vehicle Communication Bus E (Open)", - "U0070" : "Vehicle Communication Bus E (Low)", - "U0071" : "Vehicle Communication Bus E (High)", - "U0072" : "Vehicle Communication Bus E (shorted to Bus E)", - "U0073" : "Control Module Communication Bus Off", - "U0074" : "Reserved by J2012", - "U0075" : "Reserved by J2012", - "U0076" : "Reserved by J2012", - "U0077" : "Reserved by J2012", - "U0078" : "Reserved by J2012", - "U0079" : "Reserved by J2012", - "U0080" : "Reserved by J2012", - "U0081" : "Reserved by J2012", - "U0082" : "Reserved by J2012", - "U0083" : "Reserved by J2012", - "U0084" : "Reserved by J2012", - "U0085" : "Reserved by J2012", - "U0086" : "Reserved by J2012", - "U0087" : "Reserved by J2012", - "U0088" : "Reserved by J2012", - "U0089" : "Reserved by J2012", - "U0090" : "Reserved by J2012", - "U0091" : "Reserved by J2012", - "U0092" : "Reserved by J2012", - "U0093" : "Reserved by J2012", - "U0094" : "Reserved by J2012", - "U0095" : "Reserved by J2012", - "U0096" : "Reserved by J2012", - "U0097" : "Reserved by J2012", - "U0098" : "Reserved by J2012", - "U0099" : "Reserved by J2012", - "U0100" : "Lost Communication With ECM/PCM A", - "U0101" : "Lost Communication with TCM", - "U0102" : "Lost Communication with Transfer Case Control Module", - "U0103" : "Lost Communication With Gear Shift Module", - "U0104" : "Lost Communication With Cruise Control Module", - "U0105" : "Lost Communication With Fuel Injector Control Module", - "U0106" : "Lost Communication With Glow Plug Control Module", - "U0107" : "Lost Communication With Throttle Actuator Control Module", - "U0108" : "Lost Communication With Alternative Fuel Control Module", - "U0109" : "Lost Communication With Fuel Pump Control Module", - "U0110" : "Lost Communication With Drive Motor Control Module", - "U0111" : "Lost Communication With Battery Energy Control Module 'A'", - "U0112" : "Lost Communication With Battery Energy Control Module 'B'", - "U0113" : "Lost Communication With Emissions Critical Control Information", - "U0114" : "Lost Communication With Four-Wheel Drive Clutch Control Module", - "U0115" : "Lost Communication With ECM/PCM B", - "U0116" : "Reserved by J2012", - "U0117" : "Reserved by J2012", - "U0118" : "Reserved by J2012", - "U0119" : "Reserved by J2012", - "U0120" : "Reserved by J2012", - "U0121" : "Lost Communication With Anti-Lock Brake System (ABS) Control Module", - "U0122" : "Lost Communication With Vehicle Dynamics Control Module", - "U0123" : "Lost Communication With Yaw Rate Sensor Module", - "U0124" : "Lost Communication With Lateral Acceleration Sensor Module", - "U0125" : "Lost Communication With Multi-axis Acceleration Sensor Module", - "U0126" : "Lost Communication With Steering Angle Sensor Module", - "U0127" : "Lost Communication With Tire Pressure Monitor Module", - "U0128" : "Lost Communication With Park Brake Control Module", - "U0129" : "Lost Communication With Brake System Control Module", - "U0130" : "Lost Communication With Steering Effort Control Module", - "U0131" : "Lost Communication With Power Steering Control Module", - "U0132" : "Lost Communication With Ride Level Control Module", - "U0133" : "Reserved by J2012", - "U0134" : "Reserved by J2012", - "U0135" : "Reserved by J2012", - "U0136" : "Reserved by J2012", - "U0137" : "Reserved by J2012", - "U0138" : "Reserved by J2012", - "U0139" : "Reserved by J2012", - "U0140" : "Lost Communication With Body Control Module", - "U0141" : "Lost Communication With Body Control Module 'A'", - "U0142" : "Lost Communication With Body Control Module 'B'", - "U0143" : "Lost Communication With Body Control Module 'C'", - "U0144" : "Lost Communication With Body Control Module 'D'", - "U0145" : "Lost Communication With Body Control Module 'E'", - "U0146" : "Lost Communication With Gateway 'A'", - "U0147" : "Lost Communication With Gateway 'B'", - "U0148" : "Lost Communication With Gateway 'C'", - "U0149" : "Lost Communication With Gateway 'D'", - "U0150" : "Lost Communication With Gateway 'E'", - "U0151" : "Lost Communication With Restraints Control Module", - "U0152" : "Lost Communication With Side Restraints Control Module Left", - "U0153" : "Lost Communication With Side Restraints Control Module Right", - "U0154" : "Lost Communication With Restraints Occupant Sensing Control Module", - "U0155" : "Lost Communication With Instrument Panel Cluster (IPC) Control Module", - "U0156" : "Lost Communication With Information Center 'A'", - "U0157" : "Lost Communication With Information Center 'B'", - "U0158" : "Lost Communication With Head Up Display", - "U0159" : "Lost Communication With Parking Assist Control Module", - "U0160" : "Lost Communication With Audible Alert Control Module", - "U0161" : "Lost Communication With Compass Module", - "U0162" : "Lost Communication With Navigation Display Module", - "U0163" : "Lost Communication With Navigation Control Module", - "U0164" : "Lost Communication With HVAC Control Module", - "U0165" : "Lost Communication With HVAC Control Module Rear", - "U0166" : "Lost Communication With Auxiliary Heater Control Module", - "U0167" : "Lost Communication With Vehicle Immobilizer Control Module", - "U0168" : "Lost Communication With Vehicle Security Control Module", - "U0169" : "Lost Communication With Sunroof Control Module", - "U0170" : "Lost Communication With 'Restraints System Sensor A'", - "U0171" : "Lost Communication With 'Restraints System Sensor B'", - "U0172" : "Lost Communication With 'Restraints System Sensor C'", - "U0173" : "Lost Communication With 'Restraints System Sensor D'", - "U0174" : "Lost Communication With 'Restraints System Sensor E'", - "U0175" : "Lost Communication With 'Restraints System Sensor F'", - "U0176" : "Lost Communication With 'Restraints System Sensor G'", - "U0177" : "Lost Communication With 'Restraints System Sensor H'", - "U0178" : "Lost Communication With 'Restraints System Sensor I'", - "U0179" : "Lost Communication With 'Restraints System Sensor J'", - "U0180" : "Lost Communication With Automatic Lighting Control Module", - "U0181" : "Lost Communication With Headlamp Leveling Control Module", - "U0182" : "Lost Communication With Lighting Control Module Front", - "U0183" : "Lost Communication With Lighting Control Module Rear", - "U0184" : "Lost Communication With Radio", - "U0185" : "Lost Communication With Antenna Control Module", - "U0186" : "Lost Communication With Audio Amplifier", - "U0187" : "Lost Communication With Digital Disc Player/Changer Module 'A'", - "U0188" : "Lost Communication With Digital Disc Player/Changer Module 'B'", - "U0189" : "Lost Communication With Digital Disc Player/Changer Module 'C'", - "U0190" : "Lost Communication With Digital Disc Player/Changer Module 'D'", - "U0191" : "Lost Communication With Television", - "U0192" : "Lost Communication With Personal Computer", - "U0193" : "Lost Communication With 'Digital Audio Control Module A'", - "U0194" : "Lost Communication With 'Digital Audio Control Module B'", - "U0195" : "Lost Communication With Subscription Entertainment Receiver Module", - "U0196" : "Lost Communication With Rear Seat Entertainment Control Module", - "U0197" : "Lost Communication With Telephone Control Module", - "U0198" : "Lost Communication With Telematic Control Module", - "U0199" : "Lost Communication With 'Door Control Module A'", - "U0200" : "Lost Communication With 'Door Control Module B'", - "U0201" : "Lost Communication With 'Door Control Module C'", - "U0202" : "Lost Communication With 'Door Control Module D'", - "U0203" : "Lost Communication With 'Door Control Module E'", - "U0204" : "Lost Communication With 'Door Control Module F'", - "U0205" : "Lost Communication With 'Door Control Module G'", - "U0206" : "Lost Communication With Folding Top Control Module", - "U0207" : "Lost Communication With Moveable Roof Control Module", - "U0208" : "Lost Communication With 'Seat Control Module A'", - "U0209" : "Lost Communication With 'Seat Control Module B'", - "U0210" : "Lost Communication With 'Seat Control Module C'", - "U0211" : "Lost Communication With 'Seat Control Module D'", - "U0212" : "Lost Communication With Steering Column Control Module", - "U0213" : "Lost Communication With Mirror Control Module", - "U0214" : "Lost Communication With Remote Function Actuation", - "U0215" : "Lost Communication With 'Door Switch A'", - "U0216" : "Lost Communication With 'Door Switch B'", - "U0217" : "Lost Communication With 'Door Switch C'", - "U0218" : "Lost Communication With 'Door Switch D'", - "U0219" : "Lost Communication With 'Door Switch E'", - "U0220" : "Lost Communication With 'Door Switch F'", - "U0221" : "Lost Communication With 'Door Switch G'", - "U0222" : "Lost Communication With 'Door Window Motor A'", - "U0223" : "Lost Communication With 'Door Window Motor B'", - "U0224" : "Lost Communication With 'Door Window Motor C'", - "U0225" : "Lost Communication With 'Door Window Motor D'", - "U0226" : "Lost Communication With 'Door Window Motor E'", - "U0227" : "Lost Communication With 'Door Window Motor F'", - "U0228" : "Lost Communication With 'Door Window Motor G'", - "U0229" : "Lost Communication With Heated Steering Wheel Module", - "U0230" : "Lost Communication With Rear Gate Module", - "U0231" : "Lost Communication With Rain Sensing Module", - "U0232" : "Lost Communication With Side Obstacle Detection Control Module Left", - "U0233" : "Lost Communication With Side Obstacle Detection Control Module Right", - "U0234" : "Lost Communication With Convenience Recall Module", - "U0235" : "Lost Communication With Cruise Control Front Distance Range Sensor", - "U0300" : "Internal Control Module Software Incompatibility", - "U0301" : "Software Incompatibility with ECM/PCM", - "U0302" : "Software Incompatibility with Transmission Control Module", - "U0303" : "Software Incompatibility with Transfer Case Control Module", - "U0304" : "Software Incompatibility with Gear Shift Control Module", - "U0305" : "Software Incompatibility with Cruise Control Module", - "U0306" : "Software Incompatibility with Fuel Injector Control Module", - "U0307" : "Software Incompatibility with Glow Plug Control Module", - "U0308" : "Software Incompatibility with Throttle Actuator Control Module", - "U0309" : "Software Incompatibility with Alternative Fuel Control Module", - "U0310" : "Software Incompatibility with Fuel Pump Control Module", - "U0311" : "Software Incompatibility with Drive Motor Control Module", - "U0312" : "Software Incompatibility with Battery Energy Control Module A", - "U0313" : "Software Incompatibility with Battery Energy Control Module B", - "U0314" : "Software Incompatibility with Four-Wheel Drive Clutch Control Module", - "U0315" : "Software Incompatibility with Anti-Lock Brake System Control Module", - "U0316" : "Software Incompatibility with Vehicle Dynamics Control Module", - "U0317" : "Software Incompatibility with Park Brake Control Module", - "U0318" : "Software Incompatibility with Brake System Control Module", - "U0319" : "Software Incompatibility with Steering Effort Control Module", - "U0320" : "Software Incompatibility with Power Steering Control Module", - "U0321" : "Software Incompatibility with Ride Level Control Module", - "U0322" : "Software Incompatibility with Body Control Module", - "U0323" : "Software Incompatibility with Instrument Panel Control Module", - "U0324" : "Software Incompatibility with HVAC Control Module", - "U0325" : "Software Incompatibility with Auxiliary Heater Control Module", - "U0326" : "Software Incompatibility with Vehicle Immobilizer Control Module", - "U0327" : "Software Incompatibility with Vehicle Security Control Module", - "U0328" : "Software Incompatibility with Steering Angle Sensor Module", - "U0329" : "Software Incompatibility with Steering Column Control Module", - "U0330" : "Software Incompatibility with Tire Pressure Monitor Module", - "U0331" : "Software Incompatibility with Body Control Module 'A'", - "U0400" : "Invalid Data Received", - "U0401" : "Invalid Data Received From ECM/PCM", - "U0402" : "Invalid Data Received From Transmission Control Module", - "U0403" : "Invalid Data Received From Transfer Case Control Module", - "U0404" : "Invalid Data Received From Gear Shift Control Module", - "U0405" : "Invalid Data Received From Cruise Control Module", - "U0406" : "Invalid Data Received From Fuel Injector Control Module", - "U0407" : "Invalid Data Received From Glow Plug Control Module", - "U0408" : "Invalid Data Received From Throttle Actuator Control Module", - "U0409" : "Invalid Data Received From Alternative Fuel Control Module", - "U0410" : "Invalid Data Received From Fuel Pump Control Module", - "U0411" : "Invalid Data Received From Drive Motor Control Module", - "U0412" : "Invalid Data Received From Battery Energy Control Module A", - "U0413" : "Invalid Data Received From Battery Energy Control Module B", - "U0414" : "Invalid Data Received From Four-Wheel Drive Clutch Control Module", - "U0415" : "Invalid Data Received From Anti-Lock Brake System Control Module", - "U0416" : "Invalid Data Received From Vehicle Dynamics Control Module", - "U0417" : "Invalid Data Received From Park Brake Control Module", - "U0418" : "Invalid Data Received From Brake System Control Module", - "U0419" : "Invalid Data Received From Steering Effort Control Module", - "U0420" : "Invalid Data Received From Power Steering Control Module", - "U0421" : "Invalid Data Received From Ride Level Control Module", - "U0422" : "Invalid Data Received From Body Control Module", - "U0423" : "Invalid Data Received From Instrument Panel Control Module", - "U0424" : "Invalid Data Received From HVAC Control Module", - "U0425" : "Invalid Data Received From Auxiliary Heater Control Module", - "U0426" : "Invalid Data Received From Vehicle Immobilizer Control Module", - "U0427" : "Invalid Data Received From Vehicle Security Control Module", - "U0428" : "Invalid Data Received From Steering Angle Sensor Module", - "U0429" : "Invalid Data Received From Steering Column Control Module", - "U0430" : "Invalid Data Received From Tire Pressure Monitor Module", - "U0431" : "Invalid Data Received From Body Control Module 'A'", + "U0001": "High Speed CAN Communication Bus", + "U0002": "High Speed CAN Communication Bus (Performance)", + "U0003": "High Speed CAN Communication Bus (Open)", + "U0004": "High Speed CAN Communication Bus (Low)", + "U0005": "High Speed CAN Communication Bus (High)", + "U0006": "High Speed CAN Communication Bus (Open)", + "U0007": "High Speed CAN Communication Bus (Low)", + "U0008": "High Speed CAN Communication Bus (High)", + "U0009": "High Speed CAN Communication Bus (shorted to Bus)", + "U0010": "Medium Speed CAN Communication Bus", + "U0011": "Medium Speed CAN Communication Bus (Performance)", + "U0012": "Medium Speed CAN Communication Bus (Open)", + "U0013": "Medium Speed CAN Communication Bus (Low)", + "U0014": "Medium Speed CAN Communication Bus (High)", + "U0015": "Medium Speed CAN Communication Bus (Open)", + "U0016": "Medium Speed CAN Communication Bus (Low)", + "U0017": "Medium Speed CAN Communication Bus (High)", + "U0018": "Medium Speed CAN Communication Bus (shorted to Bus)", + "U0019": "Low Speed CAN Communication Bus", + "U0020": "Low Speed CAN Communication Bus (Performance)", + "U0021": "Low Speed CAN Communication Bus (Open)", + "U0022": "Low Speed CAN Communication Bus (Low)", + "U0023": "Low Speed CAN Communication Bus (High)", + "U0024": "Low Speed CAN Communication Bus (Open)", + "U0025": "Low Speed CAN Communication Bus (Low)", + "U0026": "Low Speed CAN Communication Bus (High)", + "U0027": "Low Speed CAN Communication Bus (shorted to Bus)", + "U0028": "Vehicle Communication Bus A", + "U0029": "Vehicle Communication Bus A (Performance)", + "U0030": "Vehicle Communication Bus A (Open)", + "U0031": "Vehicle Communication Bus A (Low)", + "U0032": "Vehicle Communication Bus A (High)", + "U0033": "Vehicle Communication Bus A (Open)", + "U0034": "Vehicle Communication Bus A (Low)", + "U0035": "Vehicle Communication Bus A (High)", + "U0036": "Vehicle Communication Bus A (shorted to Bus A)", + "U0037": "Vehicle Communication Bus B", + "U0038": "Vehicle Communication Bus B (Performance)", + "U0039": "Vehicle Communication Bus B (Open)", + "U0040": "Vehicle Communication Bus B (Low)", + "U0041": "Vehicle Communication Bus B (High)", + "U0042": "Vehicle Communication Bus B (Open)", + "U0043": "Vehicle Communication Bus B (Low)", + "U0044": "Vehicle Communication Bus B (High)", + "U0045": "Vehicle Communication Bus B (shorted to Bus B)", + "U0046": "Vehicle Communication Bus C", + "U0047": "Vehicle Communication Bus C (Performance)", + "U0048": "Vehicle Communication Bus C (Open)", + "U0049": "Vehicle Communication Bus C (Low)", + "U0050": "Vehicle Communication Bus C (High)", + "U0051": "Vehicle Communication Bus C (Open)", + "U0052": "Vehicle Communication Bus C (Low)", + "U0053": "Vehicle Communication Bus C (High)", + "U0054": "Vehicle Communication Bus C (shorted to Bus C)", + "U0055": "Vehicle Communication Bus D", + "U0056": "Vehicle Communication Bus D (Performance)", + "U0057": "Vehicle Communication Bus D (Open)", + "U0058": "Vehicle Communication Bus D (Low)", + "U0059": "Vehicle Communication Bus D (High)", + "U0060": "Vehicle Communication Bus D (Open)", + "U0061": "Vehicle Communication Bus D (Low)", + "U0062": "Vehicle Communication Bus D (High)", + "U0063": "Vehicle Communication Bus D (shorted to Bus D)", + "U0064": "Vehicle Communication Bus E", + "U0065": "Vehicle Communication Bus E (Performance)", + "U0066": "Vehicle Communication Bus E (Open)", + "U0067": "Vehicle Communication Bus E (Low)", + "U0068": "Vehicle Communication Bus E (High)", + "U0069": "Vehicle Communication Bus E (Open)", + "U0070": "Vehicle Communication Bus E (Low)", + "U0071": "Vehicle Communication Bus E (High)", + "U0072": "Vehicle Communication Bus E (shorted to Bus E)", + "U0073": "Control Module Communication Bus Off", + "U0074": "Reserved by J2012", + "U0075": "Reserved by J2012", + "U0076": "Reserved by J2012", + "U0077": "Reserved by J2012", + "U0078": "Reserved by J2012", + "U0079": "Reserved by J2012", + "U0080": "Reserved by J2012", + "U0081": "Reserved by J2012", + "U0082": "Reserved by J2012", + "U0083": "Reserved by J2012", + "U0084": "Reserved by J2012", + "U0085": "Reserved by J2012", + "U0086": "Reserved by J2012", + "U0087": "Reserved by J2012", + "U0088": "Reserved by J2012", + "U0089": "Reserved by J2012", + "U0090": "Reserved by J2012", + "U0091": "Reserved by J2012", + "U0092": "Reserved by J2012", + "U0093": "Reserved by J2012", + "U0094": "Reserved by J2012", + "U0095": "Reserved by J2012", + "U0096": "Reserved by J2012", + "U0097": "Reserved by J2012", + "U0098": "Reserved by J2012", + "U0099": "Reserved by J2012", + "U0100": "Lost Communication With ECM/PCM A", + "U0101": "Lost Communication with TCM", + "U0102": "Lost Communication with Transfer Case Control Module", + "U0103": "Lost Communication With Gear Shift Module", + "U0104": "Lost Communication With Cruise Control Module", + "U0105": "Lost Communication With Fuel Injector Control Module", + "U0106": "Lost Communication With Glow Plug Control Module", + "U0107": "Lost Communication With Throttle Actuator Control Module", + "U0108": "Lost Communication With Alternative Fuel Control Module", + "U0109": "Lost Communication With Fuel Pump Control Module", + "U0110": "Lost Communication With Drive Motor Control Module", + "U0111": "Lost Communication With Battery Energy Control Module 'A'", + "U0112": "Lost Communication With Battery Energy Control Module 'B'", + "U0113": "Lost Communication With Emissions Critical Control Information", + "U0114": "Lost Communication With Four-Wheel Drive Clutch Control Module", + "U0115": "Lost Communication With ECM/PCM B", + "U0116": "Reserved by J2012", + "U0117": "Reserved by J2012", + "U0118": "Reserved by J2012", + "U0119": "Reserved by J2012", + "U0120": "Reserved by J2012", + "U0121": "Lost Communication With Anti-Lock Brake System (ABS) Control Module", + "U0122": "Lost Communication With Vehicle Dynamics Control Module", + "U0123": "Lost Communication With Yaw Rate Sensor Module", + "U0124": "Lost Communication With Lateral Acceleration Sensor Module", + "U0125": "Lost Communication With Multi-axis Acceleration Sensor Module", + "U0126": "Lost Communication With Steering Angle Sensor Module", + "U0127": "Lost Communication With Tire Pressure Monitor Module", + "U0128": "Lost Communication With Park Brake Control Module", + "U0129": "Lost Communication With Brake System Control Module", + "U0130": "Lost Communication With Steering Effort Control Module", + "U0131": "Lost Communication With Power Steering Control Module", + "U0132": "Lost Communication With Ride Level Control Module", + "U0133": "Reserved by J2012", + "U0134": "Reserved by J2012", + "U0135": "Reserved by J2012", + "U0136": "Reserved by J2012", + "U0137": "Reserved by J2012", + "U0138": "Reserved by J2012", + "U0139": "Reserved by J2012", + "U0140": "Lost Communication With Body Control Module", + "U0141": "Lost Communication With Body Control Module 'A'", + "U0142": "Lost Communication With Body Control Module 'B'", + "U0143": "Lost Communication With Body Control Module 'C'", + "U0144": "Lost Communication With Body Control Module 'D'", + "U0145": "Lost Communication With Body Control Module 'E'", + "U0146": "Lost Communication With Gateway 'A'", + "U0147": "Lost Communication With Gateway 'B'", + "U0148": "Lost Communication With Gateway 'C'", + "U0149": "Lost Communication With Gateway 'D'", + "U0150": "Lost Communication With Gateway 'E'", + "U0151": "Lost Communication With Restraints Control Module", + "U0152": "Lost Communication With Side Restraints Control Module Left", + "U0153": "Lost Communication With Side Restraints Control Module Right", + "U0154": "Lost Communication With Restraints Occupant Sensing Control Module", + "U0155": "Lost Communication With Instrument Panel Cluster (IPC) Control Module", + "U0156": "Lost Communication With Information Center 'A'", + "U0157": "Lost Communication With Information Center 'B'", + "U0158": "Lost Communication With Head Up Display", + "U0159": "Lost Communication With Parking Assist Control Module", + "U0160": "Lost Communication With Audible Alert Control Module", + "U0161": "Lost Communication With Compass Module", + "U0162": "Lost Communication With Navigation Display Module", + "U0163": "Lost Communication With Navigation Control Module", + "U0164": "Lost Communication With HVAC Control Module", + "U0165": "Lost Communication With HVAC Control Module Rear", + "U0166": "Lost Communication With Auxiliary Heater Control Module", + "U0167": "Lost Communication With Vehicle Immobilizer Control Module", + "U0168": "Lost Communication With Vehicle Security Control Module", + "U0169": "Lost Communication With Sunroof Control Module", + "U0170": "Lost Communication With 'Restraints System Sensor A'", + "U0171": "Lost Communication With 'Restraints System Sensor B'", + "U0172": "Lost Communication With 'Restraints System Sensor C'", + "U0173": "Lost Communication With 'Restraints System Sensor D'", + "U0174": "Lost Communication With 'Restraints System Sensor E'", + "U0175": "Lost Communication With 'Restraints System Sensor F'", + "U0176": "Lost Communication With 'Restraints System Sensor G'", + "U0177": "Lost Communication With 'Restraints System Sensor H'", + "U0178": "Lost Communication With 'Restraints System Sensor I'", + "U0179": "Lost Communication With 'Restraints System Sensor J'", + "U0180": "Lost Communication With Automatic Lighting Control Module", + "U0181": "Lost Communication With Headlamp Leveling Control Module", + "U0182": "Lost Communication With Lighting Control Module Front", + "U0183": "Lost Communication With Lighting Control Module Rear", + "U0184": "Lost Communication With Radio", + "U0185": "Lost Communication With Antenna Control Module", + "U0186": "Lost Communication With Audio Amplifier", + "U0187": "Lost Communication With Digital Disc Player/Changer Module 'A'", + "U0188": "Lost Communication With Digital Disc Player/Changer Module 'B'", + "U0189": "Lost Communication With Digital Disc Player/Changer Module 'C'", + "U0190": "Lost Communication With Digital Disc Player/Changer Module 'D'", + "U0191": "Lost Communication With Television", + "U0192": "Lost Communication With Personal Computer", + "U0193": "Lost Communication With 'Digital Audio Control Module A'", + "U0194": "Lost Communication With 'Digital Audio Control Module B'", + "U0195": "Lost Communication With Subscription Entertainment Receiver Module", + "U0196": "Lost Communication With Rear Seat Entertainment Control Module", + "U0197": "Lost Communication With Telephone Control Module", + "U0198": "Lost Communication With Telematic Control Module", + "U0199": "Lost Communication With 'Door Control Module A'", + "U0200": "Lost Communication With 'Door Control Module B'", + "U0201": "Lost Communication With 'Door Control Module C'", + "U0202": "Lost Communication With 'Door Control Module D'", + "U0203": "Lost Communication With 'Door Control Module E'", + "U0204": "Lost Communication With 'Door Control Module F'", + "U0205": "Lost Communication With 'Door Control Module G'", + "U0206": "Lost Communication With Folding Top Control Module", + "U0207": "Lost Communication With Moveable Roof Control Module", + "U0208": "Lost Communication With 'Seat Control Module A'", + "U0209": "Lost Communication With 'Seat Control Module B'", + "U0210": "Lost Communication With 'Seat Control Module C'", + "U0211": "Lost Communication With 'Seat Control Module D'", + "U0212": "Lost Communication With Steering Column Control Module", + "U0213": "Lost Communication With Mirror Control Module", + "U0214": "Lost Communication With Remote Function Actuation", + "U0215": "Lost Communication With 'Door Switch A'", + "U0216": "Lost Communication With 'Door Switch B'", + "U0217": "Lost Communication With 'Door Switch C'", + "U0218": "Lost Communication With 'Door Switch D'", + "U0219": "Lost Communication With 'Door Switch E'", + "U0220": "Lost Communication With 'Door Switch F'", + "U0221": "Lost Communication With 'Door Switch G'", + "U0222": "Lost Communication With 'Door Window Motor A'", + "U0223": "Lost Communication With 'Door Window Motor B'", + "U0224": "Lost Communication With 'Door Window Motor C'", + "U0225": "Lost Communication With 'Door Window Motor D'", + "U0226": "Lost Communication With 'Door Window Motor E'", + "U0227": "Lost Communication With 'Door Window Motor F'", + "U0228": "Lost Communication With 'Door Window Motor G'", + "U0229": "Lost Communication With Heated Steering Wheel Module", + "U0230": "Lost Communication With Rear Gate Module", + "U0231": "Lost Communication With Rain Sensing Module", + "U0232": "Lost Communication With Side Obstacle Detection Control Module Left", + "U0233": "Lost Communication With Side Obstacle Detection Control Module Right", + "U0234": "Lost Communication With Convenience Recall Module", + "U0235": "Lost Communication With Cruise Control Front Distance Range Sensor", + "U0300": "Internal Control Module Software Incompatibility", + "U0301": "Software Incompatibility with ECM/PCM", + "U0302": "Software Incompatibility with Transmission Control Module", + "U0303": "Software Incompatibility with Transfer Case Control Module", + "U0304": "Software Incompatibility with Gear Shift Control Module", + "U0305": "Software Incompatibility with Cruise Control Module", + "U0306": "Software Incompatibility with Fuel Injector Control Module", + "U0307": "Software Incompatibility with Glow Plug Control Module", + "U0308": "Software Incompatibility with Throttle Actuator Control Module", + "U0309": "Software Incompatibility with Alternative Fuel Control Module", + "U0310": "Software Incompatibility with Fuel Pump Control Module", + "U0311": "Software Incompatibility with Drive Motor Control Module", + "U0312": "Software Incompatibility with Battery Energy Control Module A", + "U0313": "Software Incompatibility with Battery Energy Control Module B", + "U0314": "Software Incompatibility with Four-Wheel Drive Clutch Control Module", + "U0315": "Software Incompatibility with Anti-Lock Brake System Control Module", + "U0316": "Software Incompatibility with Vehicle Dynamics Control Module", + "U0317": "Software Incompatibility with Park Brake Control Module", + "U0318": "Software Incompatibility with Brake System Control Module", + "U0319": "Software Incompatibility with Steering Effort Control Module", + "U0320": "Software Incompatibility with Power Steering Control Module", + "U0321": "Software Incompatibility with Ride Level Control Module", + "U0322": "Software Incompatibility with Body Control Module", + "U0323": "Software Incompatibility with Instrument Panel Control Module", + "U0324": "Software Incompatibility with HVAC Control Module", + "U0325": "Software Incompatibility with Auxiliary Heater Control Module", + "U0326": "Software Incompatibility with Vehicle Immobilizer Control Module", + "U0327": "Software Incompatibility with Vehicle Security Control Module", + "U0328": "Software Incompatibility with Steering Angle Sensor Module", + "U0329": "Software Incompatibility with Steering Column Control Module", + "U0330": "Software Incompatibility with Tire Pressure Monitor Module", + "U0331": "Software Incompatibility with Body Control Module 'A'", + "U0400": "Invalid Data Received", + "U0401": "Invalid Data Received From ECM/PCM", + "U0402": "Invalid Data Received From Transmission Control Module", + "U0403": "Invalid Data Received From Transfer Case Control Module", + "U0404": "Invalid Data Received From Gear Shift Control Module", + "U0405": "Invalid Data Received From Cruise Control Module", + "U0406": "Invalid Data Received From Fuel Injector Control Module", + "U0407": "Invalid Data Received From Glow Plug Control Module", + "U0408": "Invalid Data Received From Throttle Actuator Control Module", + "U0409": "Invalid Data Received From Alternative Fuel Control Module", + "U0410": "Invalid Data Received From Fuel Pump Control Module", + "U0411": "Invalid Data Received From Drive Motor Control Module", + "U0412": "Invalid Data Received From Battery Energy Control Module A", + "U0413": "Invalid Data Received From Battery Energy Control Module B", + "U0414": "Invalid Data Received From Four-Wheel Drive Clutch Control Module", + "U0415": "Invalid Data Received From Anti-Lock Brake System Control Module", + "U0416": "Invalid Data Received From Vehicle Dynamics Control Module", + "U0417": "Invalid Data Received From Park Brake Control Module", + "U0418": "Invalid Data Received From Brake System Control Module", + "U0419": "Invalid Data Received From Steering Effort Control Module", + "U0420": "Invalid Data Received From Power Steering Control Module", + "U0421": "Invalid Data Received From Ride Level Control Module", + "U0422": "Invalid Data Received From Body Control Module", + "U0423": "Invalid Data Received From Instrument Panel Control Module", + "U0424": "Invalid Data Received From HVAC Control Module", + "U0425": "Invalid Data Received From Auxiliary Heater Control Module", + "U0426": "Invalid Data Received From Vehicle Immobilizer Control Module", + "U0427": "Invalid Data Received From Vehicle Security Control Module", + "U0428": "Invalid Data Received From Steering Angle Sensor Module", + "U0429": "Invalid Data Received From Steering Column Control Module", + "U0430": "Invalid Data Received From Tire Pressure Monitor Module", + "U0431": "Invalid Data Received From Body Control Module 'A'", } IGNITION_TYPE = [ @@ -2214,18 +2213,18 @@ ] TEST_IDS = { - # : + # : (, ) # 0x0 is reserved - 0x01 : ("RTL_THRESHOLD_VOLTAGE", "Rich to lean sensor threshold voltage"), - 0x02 : ("LTR_THRESHOLD_VOLTAGE", "Lean to rich sensor threshold voltage"), - 0x03 : ("LOW_VOLTAGE_SWITCH_TIME", "Low sensor voltage for switch time calculation"), - 0x04 : ("HIGH_VOLTAGE_SWITCH_TIME", "High sensor voltage for switch time calculation"), - 0x05 : ("RTL_SWITCH_TIME", "Rich to lean sensor switch time"), - 0x06 : ("LTR_SWITCH_TIME", "Lean to rich sensor switch time"), - 0x07 : ("MIN_VOLTAGE", "Minimum sensor voltage for test cycle"), - 0x08 : ("MAX_VOLTAGE", "Maximum sensor voltage for test cycle"), - 0x09 : ("TRANSITION_TIME", "Time between sensor transitions"), - 0x0A : ("SENSOR_PERIOD", "Sensor period"), - 0x0B : ("MISFIRE_AVERAGE", "Average misfire counts for last ten driving cycles"), - 0x0C : ("MISFIRE_COUNT", "Misfire counts for last/current driving cycles"), + 0x01: ("RTL_THRESHOLD_VOLTAGE", "Rich to lean sensor threshold voltage"), + 0x02: ("LTR_THRESHOLD_VOLTAGE", "Lean to rich sensor threshold voltage"), + 0x03: ("LOW_VOLTAGE_SWITCH_TIME", "Low sensor voltage for switch time calculation"), + 0x04: ("HIGH_VOLTAGE_SWITCH_TIME", "High sensor voltage for switch time calculation"), + 0x05: ("RTL_SWITCH_TIME", "Rich to lean sensor switch time"), + 0x06: ("LTR_SWITCH_TIME", "Lean to rich sensor switch time"), + 0x07: ("MIN_VOLTAGE", "Minimum sensor voltage for test cycle"), + 0x08: ("MAX_VOLTAGE", "Maximum sensor voltage for test cycle"), + 0x09: ("TRANSITION_TIME", "Time between sensor transitions"), + 0x0A: ("SENSOR_PERIOD", "Sensor period"), + 0x0B: ("MISFIRE_AVERAGE", "Average misfire counts for last ten driving cycles"), + 0x0C: ("MISFIRE_COUNT", "Misfire counts for last/current driving cycles"), } diff --git a/obd/commands.py b/obd/commands.py index cf0841aa..43ec8f3c 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -30,16 +30,15 @@ # # ######################################################################## -from .protocols import ECU +import logging + from .OBDCommand import OBDCommand from .decoders import * - -import logging +from .protocols import ECU logger = logging.getLogger(__name__) - - +# flake8: noqa ''' Define command tables ''' @@ -153,27 +152,23 @@ OBDCommand("EMISSION_REQ" , "Designed emission requirements" , b"015F", 3, drop, ECU.ENGINE, True), ] - # mode 2 is the same as mode 1, but returns values from when the DTC occured __mode2__ = [] for c in __mode1__: c = c.clone() - c.command = b"02" + c.command[2:] # change the mode: 0100 ---> 0200 + c.command = b"02" + c.command[2:] # change the mode: 0100 ---> 0200 c.name = "DTC_" + c.name c.desc = "DTC " + c.desc if c.decode == pid: - c.decode = drop # Never send mode 02 pid requests (use mode 01 instead) + c.decode = drop # Never send mode 02 pid requests (use mode 01 instead) __mode2__.append(c) - __mode3__ = [ - # name description cmd bytes decoder ECU fast - OBDCommand("GET_DTC" , "Get DTCs" , b"03", 0, dtc, ECU.ALL, False), + OBDCommand("GET_DTC", "Get DTCs", b"03", 0, dtc, ECU.ALL, False), ] __mode4__ = [ - # name description cmd bytes decoder ECU fast - OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , b"04", 0, drop, ECU.ALL, False), + OBDCommand("CLEAR_DTC", "Clear DTCs and Freeze data", b"04", 0, drop, ECU.ALL, False), ] __mode6__ = [ @@ -281,28 +276,18 @@ ] __mode7__ = [ - # name description cmd bytes decoder ECU fast - OBDCommand("GET_CURRENT_DTC" , "Get DTCs from the current/last driving cycle" , b"07", 0, dtc, ECU.ALL, False), -] - -__mode9__ = [ - # name description cmd bytes decoder ECU fast - # OBDCommand("PIDS_9A" , "Supported PIDs [01-20]" , b"0900", 4, pid, ECU.ENGINE, True), - # OBDCommand("VIN_MESSAGE_COUNT" , "VIN Message Count" , b"0901", 1, uas(0x01), ECU.ENGINE, True), - # OBDCommand("VIN" , "Get Vehicle Identification Number" , b"0902", 20, raw_string, ECU.ENGINE, True), + OBDCommand("GET_CURRENT_DTC", "Get DTCs from the current/last driving cycle", b"07", 0, dtc, ECU.ALL, False), ] __misc__ = [ - # name description cmd bytes decoder ECU fast - OBDCommand("ELM_VERSION" , "ELM327 version string" , b"ATI", 0, raw_string, ECU.UNKNOWN, False), - OBDCommand("ELM_VOLTAGE" , "Voltage detected by OBD-II adapter" , b"ATRV", 0, elm_voltage, ECU.UNKNOWN, False), + OBDCommand("ELM_VERSION", "ELM327 version string", b"ATI", 0, raw_string, ECU.UNKNOWN, False), + OBDCommand("ELM_VOLTAGE", "Voltage detected by OBD-II adapter", b"ATRV", 0, elm_voltage, ECU.UNKNOWN, False), ] - - -''' +""" Assemble the command tables by mode, and allow access by name -''' +""" + class Commands(): def __init__(self): @@ -318,7 +303,6 @@ def __init__(self): __mode6__, __mode7__, [], - __mode9__, ] # allow commands to be accessed by name @@ -330,7 +314,6 @@ def __init__(self): for c in __misc__: self.__dict__[c.name] = c - def __getitem__(self, key): """ commands can be accessed by name, or by mode/pid @@ -352,17 +335,14 @@ def __getitem__(self, key): else: logger.warning("OBD commands can only be retrieved by PID value or dict name") - def __len__(self): """ returns the number of commands supported by python-OBD """ return sum([len(mode) for mode in self.modes]) - def __contains__(self, name): """ calls has_name(s) """ return self.has_name(name) - def base_commands(self): """ returns the list of commands that should always be @@ -378,26 +358,22 @@ def base_commands(self): self.ELM_VOLTAGE, ] - def pid_getters(self): """ returns a list of PID GET commands """ getters = [] for mode in self.modes: - getters += [ cmd for cmd in mode if (cmd and cmd.decode == pid) ] + getters += [cmd for cmd in mode if (cmd and cmd.decode == pid)] return getters - def has_command(self, c): """ checks for existance of a command by OBDCommand object """ return c in self.__dict__.values() - def has_name(self, name): """ checks for existance of a command by name """ # isupper() rejects all the normal properties return name.isupper() and name in self.__dict__ - def has_pid(self, mode, pid): """ checks for existance of a command by int mode and int pid """ if (mode < 0) or (pid < 0): @@ -408,7 +384,7 @@ def has_pid(self, mode, pid): return False # make sure that the command isn't reserved - return (self.modes[mode][pid] is not None) + return self.modes[mode][pid] is not None # export this object diff --git a/obd/decoders.py b/obd/decoders.py index 83099dd4..3020be0c 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -51,9 +51,8 @@ def (): ''' - # drop all messages, return None -def drop(messages): +def drop(_): return None @@ -65,7 +64,8 @@ def noop(messages): # hex in, bitstring out def pid(messages): d = messages[0].data[2:] - return bitarray(d) + return BitArray(d) + # returns the raw strings from the ELM def raw_string(messages): @@ -78,13 +78,15 @@ def raw_string(messages): Unit/Scaling in that table, simply to avoid redundant code. """ -def uas(id): + +def uas(id_): """ get the corresponding decoder for this UAS ID """ - return functools.partial(decode_uas, id=id) + return functools.partial(decode_uas, id_=id_) + -def decode_uas(messages, id): - d = messages[0].data[2:] # chop off mode and PID bytes - return UAS_IDS[id](d) +def decode_uas(messages, id_): + d = messages[0].data[2:] # chop off mode and PID bytes + return UAS_IDS[id_](d) """ @@ -92,6 +94,7 @@ def decode_uas(messages, id): Return pint Quantities """ + # 0 to 100 % def percent(messages): d = messages[0].data[2:] @@ -99,6 +102,7 @@ def percent(messages): v = v * 100.0 / 255.0 return v * Unit.percent + # -100 to 100 % def percent_centered(messages): d = messages[0].data[2:] @@ -106,12 +110,14 @@ def percent_centered(messages): v = (v - 128) * 100.0 / 128.0 return v * Unit.percent + # -40 to 215 C def temp(messages): d = messages[0].data[2:] v = bytes_to_int(d) v = v - 40 - return Unit.Quantity(v, Unit.celsius) # non-multiplicative unit + return Unit.Quantity(v, Unit.celsius) # non-multiplicative unit + # -128 to 128 mA def current_centered(messages): @@ -120,12 +126,14 @@ def current_centered(messages): v = (v / 256.0) - 128 return v * Unit.milliampere + # 0 to 1.275 volts def sensor_voltage(messages): d = messages[0].data[2:] v = d[0] / 200.0 return v * Unit.volt + # 0 to 8 volts def sensor_voltage_big(messages): d = messages[0].data[2:] @@ -133,6 +141,7 @@ def sensor_voltage_big(messages): v = (v * 8.0) / 65535 return v * Unit.volt + # 0 to 765 kPa def fuel_pressure(messages): d = messages[0].data[2:] @@ -140,12 +149,14 @@ def fuel_pressure(messages): v = v * 3 return v * Unit.kilopascal + # 0 to 255 kPa def pressure(messages): d = messages[0].data[2:] v = d[0] return v * Unit.kilopascal + # -8192 to 8192 Pa def evap_pressure(messages): # decode the twos complement @@ -155,6 +166,7 @@ def evap_pressure(messages): v = ((a * 256.0) + b) / 4.0 return v * Unit.pascal + # 0 to 327.675 kPa def abs_evap_pressure(messages): d = messages[0].data[2:] @@ -162,6 +174,7 @@ def abs_evap_pressure(messages): v = v / 200.0 return v * Unit.kilopascal + # -32767 to 32768 Pa def evap_pressure_alt(messages): d = messages[0].data[2:] @@ -169,6 +182,7 @@ def evap_pressure_alt(messages): v = v - 32767 return v * Unit.pascal + # -64 to 63.5 degrees def timing_advance(messages): d = messages[0].data[2:] @@ -176,6 +190,7 @@ def timing_advance(messages): v = (v - 128) / 2.0 return v * Unit.degree + # -210 to 301 degrees def inject_timing(messages): d = messages[0].data[2:] @@ -183,6 +198,7 @@ def inject_timing(messages): v = (v - 26880) / 128.0 return v * Unit.degree + # 0 to 2550 grams/sec def max_maf(messages): d = messages[0].data[2:] @@ -190,6 +206,7 @@ def max_maf(messages): v = v * 10 return v * Unit.gps + # 0 to 3212 Liters/hour def fuel_rate(messages): d = messages[0].data[2:] @@ -197,32 +214,36 @@ def fuel_rate(messages): v = v * 0.05 return v * Unit.liters_per_hour + # special bit encoding for PID 13 def o2_sensors(messages): d = messages[0].data[2:] - bits = bitarray(d) + bits = BitArray(d) return ( - (), # bank 0 is invalid - tuple(bits[:4]), # bank 1 - tuple(bits[4:]), # bank 2 + (), # bank 0 is invalid + tuple(bits[:4]), # bank 1 + tuple(bits[4:]), # bank 2 ) + def aux_input_status(messages): d = messages[0].data[2:] - return ((d[0] >> 7) & 1) == 1 # first bit indicate PTO status + return ((d[0] >> 7) & 1) == 1 # first bit indicate PTO status + # special bit encoding for PID 1D def o2_sensors_alt(messages): d = messages[0].data[2:] - bits = bitarray(d) + bits = BitArray(d) return ( - (), # bank 0 is invalid - tuple(bits[:2]), # bank 1 - tuple(bits[2:4]), # bank 2 - tuple(bits[4:6]), # bank 3 - tuple(bits[6:]), # bank 4 + (), # bank 0 is invalid + tuple(bits[:2]), # bank 1 + tuple(bits[2:4]), # bank 2 + tuple(bits[4:6]), # bank 3 + tuple(bits[6:]), # bank 4 ) + # 0 to 25700 % def absolute_load(messages): d = messages[0].data[2:] @@ -230,6 +251,7 @@ def absolute_load(messages): v *= 100.0 / 255.0 return v * Unit.percent + def elm_voltage(messages): # doesn't register as a normal OBD response, # so access the raw frame data @@ -251,10 +273,9 @@ def elm_voltage(messages): ''' - def status(messages): d = messages[0].data[2:] - bits = bitarray(d) + bits = BitArray(d) # ┌Components not ready # |┌Fuel not ready @@ -278,32 +299,31 @@ def status(messages): output.__dict__[name] = t # different tests for different ignition types - if bits[12]: # compression - for i, name in enumerate(COMPRESSION_TESTS[::-1]): # reverse to correct for bit vs. indexing order + if bits[12]: # compression + for i, name in enumerate(COMPRESSION_TESTS[::-1]): # reverse to correct for bit vs. indexing order t = StatusTest(name, bits[(2 * 8) + i], - not bits[(3 * 8) + i]) + not bits[(3 * 8) + i]) output.__dict__[name] = t - else: # spark - for i, name in enumerate(SPARK_TESTS[::-1]): # reverse to correct for bit vs. indexing order + else: # spark + for i, name in enumerate(SPARK_TESTS[::-1]): # reverse to correct for bit vs. indexing order t = StatusTest(name, bits[(2 * 8) + i], - not bits[(3 * 8) + i]) + not bits[(3 * 8) + i]) output.__dict__[name] = t return output - def fuel_status(messages): d = messages[0].data[2:] - bits = bitarray(d) + bits = BitArray(d) status_1 = "" status_2 = "" if bits[0:8].count(True) == 1: if 7 - bits[0:8].index(True) < len(FUEL_STATUS): - status_1 = FUEL_STATUS[ 7 - bits[0:8].index(True) ] + status_1 = FUEL_STATUS[7 - bits[0:8].index(True)] else: logger.debug("Invalid response for fuel status (high bits set)") else: @@ -311,7 +331,7 @@ def fuel_status(messages): if bits[8:16].count(True) == 1: if 7 - bits[8:16].index(True) < len(FUEL_STATUS): - status_2 = FUEL_STATUS[ 7 - bits[8:16].index(True) ] + status_2 = FUEL_STATUS[7 - bits[8:16].index(True)] else: logger.debug("Invalid response for fuel status (high bits set)") else: @@ -325,11 +345,11 @@ def fuel_status(messages): def air_status(messages): d = messages[0].data[2:] - bits = bitarray(d) + bits = BitArray(d) status = None if bits.num_set() == 1: - status = AIR_STATUS[ 7 - bits[0:8].index(True) ] + status = AIR_STATUS[7 - bits[0:8].index(True)] else: logger.debug("Invalid response for fuel status (multiple/no bits set)") @@ -352,7 +372,7 @@ def obd_compliance(messages): def fuel_type(messages): d = messages[0].data[2:] - i = d[0] # todo, support second fuel system + i = d[0] # todo, support second fuel system v = None @@ -368,7 +388,7 @@ def parse_dtc(_bytes): """ converts 2 bytes into a DTC code """ # check validity (also ignores padding that the ELM returns) - if (len(_bytes) != 2) or (_bytes == (0,0)): + if (len(_bytes) != 2) or (_bytes == (0, 0)): return None # BYTES: (16, 35 ) @@ -378,8 +398,8 @@ def parse_dtc(_bytes): # | / / # DTC: C0123 - dtc = ['P', 'C', 'B', 'U'][ _bytes[0] >> 6 ] # the last 2 bits of the first byte - dtc += str( (_bytes[0] >> 4) & 0b0011 ) # the next pair of 2 bits. Mask off the bits we read above + dtc = ['P', 'C', 'B', 'U'][_bytes[0] >> 6] # the last 2 bits of the first byte + dtc += str((_bytes[0] >> 4) & 0b0011) # the next pair of 2 bits. Mask off the bits we read above dtc += bytes_to_hex(_bytes)[1:4] # pull a description if we have one @@ -397,14 +417,14 @@ def dtc(messages): codes = [] d = [] for message in messages: - d += message.data[2:] # remove the mode and DTC_count bytes + d += message.data[2:] # remove the mode and DTC_count bytes # look at data in pairs of bytes # looping through ENDING indices to avoid odd (invalid) code lengths for n in range(1, len(d), 2): # parse the code - dtc = parse_dtc( (d[n-1], d[n]) ) + dtc = parse_dtc((d[n - 1], d[n])) if dtc is not None: codes.append(dtc) @@ -418,8 +438,8 @@ def parse_monitor_test(d, mon): tid = d[1] if tid in TEST_IDS: - test.name = TEST_IDS[tid][0] # lookup the name from the table - test.desc = TEST_IDS[tid][1] # lookup the description from the table + test.name = TEST_IDS[tid][0] # lookup the name from the table + test.desc = TEST_IDS[tid][1] # lookup the description from the table else: logger.debug("Encountered unknown Test ID") test.name = "Unknown" @@ -434,18 +454,19 @@ def parse_monitor_test(d, mon): # load the test results test.tid = tid - test.value = uas(d[3:5]) # convert bytes to actual values - test.min = uas(d[5:7]) - test.max = uas(d[7:]) + test.value = uas(d[3:5]) # convert bytes to actual values + test.min = uas(d[5:7]) + test.max = uas(d[7:]) return test def monitor(messages): - d = messages[0].data[1:] # only dispose of the mode byte. Leave the MID - # even though we never use the MID byte, it may - # show up multiple times. Thus, keeping it make - # for easier parsing. + d = messages[0].data[1:] + # only dispose of the mode byte. Leave the MID + # even though we never use the MID byte, it may + # show up multiple times. Thus, keeping it make + # for easier parsing. mon = Monitor() # test that we got the right number of bytes diff --git a/obd/elm327.py b/obd/elm327.py index d8b6b17e..4a865a95 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -58,35 +58,36 @@ class ELM327: ELM_PROMPT = b'>' _SUPPORTED_PROTOCOLS = { - #"0" : None, # Automatic Mode. This isn't an actual protocol. If the - # ELM reports this, then we don't have enough - # information. see auto_protocol() - "1" : SAE_J1850_PWM, - "2" : SAE_J1850_VPW, - "3" : ISO_9141_2, - "4" : ISO_14230_4_5baud, - "5" : ISO_14230_4_fast, - "6" : ISO_15765_4_11bit_500k, - "7" : ISO_15765_4_29bit_500k, - "8" : ISO_15765_4_11bit_250k, - "9" : ISO_15765_4_29bit_250k, - "A" : SAE_J1939, - #"B" : None, # user defined 1 - #"C" : None, # user defined 2 + # "0" : None, + # Automatic Mode. This isn't an actual protocol. If the + # ELM reports this, then we don't have enough + # information. see auto_protocol() + "1": SAE_J1850_PWM, + "2": SAE_J1850_VPW, + "3": ISO_9141_2, + "4": ISO_14230_4_5baud, + "5": ISO_14230_4_fast, + "6": ISO_15765_4_11bit_500k, + "7": ISO_15765_4_29bit_500k, + "8": ISO_15765_4_11bit_250k, + "9": ISO_15765_4_29bit_250k, + "A": SAE_J1939, + # "B" : None, # user defined 1 + # "C" : None, # user defined 2 } # used as a fallback, when ATSP0 doesn't cut it _TRY_PROTOCOL_ORDER = [ - "6", # ISO_15765_4_11bit_500k - "8", # ISO_15765_4_11bit_250k - "1", # SAE_J1850_PWM - "7", # ISO_15765_4_29bit_500k - "9", # ISO_15765_4_29bit_250k - "2", # SAE_J1850_VPW - "3", # ISO_9141_2 - "4", # ISO_14230_4_5baud - "5", # ISO_14230_4_fast - "A", # SAE_J1939 + "6", # ISO_15765_4_11bit_500k + "8", # ISO_15765_4_11bit_250k + "1", # SAE_J1850_PWM + "7", # ISO_15765_4_29bit_500k + "9", # ISO_15765_4_29bit_250k + "2", # SAE_J1850_VPW + "3", # ISO_9141_2 + "4", # ISO_14230_4_5baud + "5", # ISO_14230_4_fast + "A", # SAE_J1939 ] # 38400, 9600 are the possible boot bauds (unless reprogrammed via @@ -99,9 +100,7 @@ class ELM327: # We check the two default baud rates first, then go fastest to # slowest, on the theory that anyone who's using a slow baud rate is # going to be less picky about the time required to detect it. - _TRY_BAUDS = [ 38400, 9600, 230400, 115200, 57600, 19200 ] - - + _TRY_BAUDS = [38400, 9600, 230400, 115200, 57600, 19200] def __init__(self, portname, baudrate, protocol, timeout, check_voltage=True): @@ -114,19 +113,18 @@ def __init__(self, portname, baudrate, protocol, timeout, "auto" if protocol is None else protocol, )) - self.__status = OBDStatus.NOT_CONNECTED - self.__port = None + self.__status = OBDStatus.NOT_CONNECTED + self.__port = None self.__protocol = UnknownProtocol([]) self.timeout = timeout - # ------------- open port ------------- try: - self.__port = serial.serial_for_url(portname, \ - parity = serial.PARITY_NONE, \ - stopbits = 1, \ - bytesize = 8, - timeout = 10) # seconds + self.__port = serial.serial_for_url(portname, + parity=serial.PARITY_NONE, + stopbits=1, + bytesize=8, + timeout=10) # seconds except serial.SerialException as e: self.__error(e) return @@ -142,7 +140,7 @@ def __init__(self, portname, baudrate, protocol, timeout, # ---------------------------- ATZ (reset) ---------------------------- try: - self.__send(b"ATZ", delay=1) # wait 1 second for ELM to initialize + self.__send(b"ATZ", delay=1) # wait 1 second for ELM to initialize # return data can be junk, so don't bother checking except serial.SerialException as e: self.__error(e) @@ -198,34 +196,31 @@ def __init__(self, portname, baudrate, protocol, timeout, if self.__status == OBDStatus.OBD_CONNECTED: logger.error("Adapter connected, but the ignition is off") else: - logger.error("Connected to the adapter, "\ + logger.error("Connected to the adapter, " "but failed to connect to the vehicle") - - def set_protocol(self, protocol): - if protocol is not None: + def set_protocol(self, protocol_): + if protocol_ is not None: # an explicit protocol was specified - if protocol not in self._SUPPORTED_PROTOCOLS: + if protocol_ not in self._SUPPORTED_PROTOCOLS: logger.error("%s is not a valid protocol. Please use \"1\" through \"A\"") return False - return self.manual_protocol(protocol) + return self.manual_protocol(protocol_) else: # auto detect the protocol return self.auto_protocol() - - def manual_protocol(self, protocol): - r = self.__send(b"ATTP" + protocol.encode()) + def manual_protocol(self, protocol_): + r = self.__send(b"ATTP" + protocol_.encode()) r0100 = self.__send(b"0100") if not self.__has_message(r0100, "UNABLE TO CONNECT"): # success, found the protocol - self.__protocol = self._SUPPORTED_PROTOCOLS[protocol](r0100) + self.__protocol = self._SUPPORTED_PROTOCOLS[protocol_](r0100) return True return False - def auto_protocol(self): """ Attempts communication with the car. @@ -251,8 +246,7 @@ def auto_protocol(self): logger.error("Failed to retrieve current protocol") return False - - p = r[0] # grab the first (and only) line returned + p = r[0] # grab the first (and only) line returned # suppress any "automatic" prefix p = p[1:] if (len(p) > 1 and p.startswith("A")) else p @@ -279,7 +273,6 @@ def auto_protocol(self): logger.error("Failed to determine protocol") return False - def set_baudrate(self, baud): if baud is None: # when connecting to pseudo terminal, don't bother with auto baud @@ -292,7 +285,6 @@ def set_baudrate(self, baud): self.__port.baudrate = baud return True - def auto_baudrate(self): """ Detect the baud rate at which a connected ELM32x interface is operating. @@ -301,7 +293,7 @@ def auto_baudrate(self): # before we change the timout, save the "normal" value timeout = self.__port.timeout - self.__port.timeout = self.timeout # we're only talking with the ELM, so things should go quickly + self.__port.timeout = self.timeout # we're only talking with the ELM, so things should go quickly for baud in self._TRY_BAUDS: self.__port.baudrate = baud @@ -324,16 +316,13 @@ def auto_baudrate(self): # watch for the prompt character if response.endswith(b">"): logger.debug("Choosing baud %d" % baud) - self.__port.timeout = timeout # reinstate our original timeout + self.__port.timeout = timeout # reinstate our original timeout return True - logger.debug("Failed to choose baud") - self.__port.timeout = timeout # reinstate our original timeout + self.__port.timeout = timeout # reinstate our original timeout return False - - def __isok(self, lines, expectEcho=False): if not lines: return False @@ -344,50 +333,42 @@ def __isok(self, lines, expectEcho=False): else: return len(lines) == 1 and lines[0] == 'OK' - def __has_message(self, lines, text): for line in lines: if text in line: return True return False - def __error(self, msg): """ handles fatal failures, print logger.info info and closes serial """ self.close() logger.error(str(msg)) - def port_name(self): if self.__port is not None: return self.__port.portstr else: return "" - def status(self): return self.__status - def ecus(self): return self.__protocol.ecu_map.values() - def protocol_name(self): return self.__protocol.ELM_NAME - def protocol_id(self): return self.__protocol.ELM_ID - def close(self): """ Resets the device, and sets all attributes to unconnected states. """ - self.__status = OBDStatus.NOT_CONNECTED + self.__status = OBDStatus.NOT_CONNECTED self.__protocol = None if self.__port is not None: @@ -396,7 +377,6 @@ def close(self): self.__port.close() self.__port = None - def send_and_parse(self, cmd): """ send() function used to service all OBDCommands @@ -417,7 +397,6 @@ def send_and_parse(self, cmd): messages = self.__protocol(lines) return messages - def __send(self, cmd, delay=None): """ unprotected send() function @@ -435,19 +414,18 @@ def __send(self, cmd, delay=None): return self.__read() - def __write(self, cmd): """ "low-level" function to write a string to the port """ if self.__port: - cmd += b"\r" # terminate with carriage return in accordance with ELM327 and STN11XX specifications + cmd += b"\r" # terminate with carriage return in accordance with ELM327 and STN11XX specifications logger.debug("write: " + repr(cmd)) try: - self.__port.flushInput() # dump everything in the input buffer - self.__port.write(cmd) # turn the string into bytes and write - self.__port.flush() # wait for the output buffer to finish transmitting + self.__port.flushInput() # dump everything in the input buffer + self.__port.write(cmd) # turn the string into bytes and write + self.__port.flush() # wait for the output buffer to finish transmitting except Exception: self.__status = OBDStatus.NOT_CONNECTED self.__port.close() @@ -457,7 +435,6 @@ def __write(self, cmd): else: logger.info("cannot perform __write() when unconnected") - def __read(self): """ "low-level" read function @@ -482,7 +459,7 @@ def __read(self): logger.critical("Device disconnected while reading") return [] - # if nothing was recieved + # if nothing was received if not data: logger.warning("Failed to read port") break @@ -507,6 +484,6 @@ def __read(self): string = buffer.decode("utf-8", "ignore") # splits into lines while removing empty lines and trailing spaces - lines = [ s.strip() for s in re.split("[\r\n]", string) if bool(s) ] + lines = [s.strip() for s in re.split("[\r\n]", string) if bool(s)] return lines diff --git a/obd/obd.py b/obd/obd.py index e831ae2b..610f13bf 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -33,12 +33,12 @@ import logging +from .OBDResponse import OBDResponse from .__version__ import __version__ -from .elm327 import ELM327 from .commands import commands -from .OBDResponse import OBDResponse -from .utils import scan_serial, OBDStatus +from .elm327 import ELM327 from .protocols import ECU_HEADER +from .utils import scan_serial, OBDStatus logger = logging.getLogger(__name__) @@ -53,19 +53,18 @@ def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True, timeout=0.1, check_voltage=True): self.interface = None self.supported_commands = set(commands.base_commands()) - self.fast = fast # global switch for disabling optimizations + self.fast = fast # global switch for disabling optimizations self.timeout = timeout - self.__last_command = b"" # used for running the previous command with a CR - self.__last_header = ECU_HEADER.ENGINE # for comparing with the previously used header - self.__frame_counts = {} # keeps track of the number of return frames for each command + self.__last_command = b"" # used for running the previous command with a CR + self.__last_header = ECU_HEADER.ENGINE # for comparing with the previously used header + self.__frame_counts = {} # keeps track of the number of return frames for each command logger.info("======================= python-OBD (v%s) =======================" % __version__) self.__connect(portstr, baudrate, protocol, - check_voltage) # initialize by connecting and loading sensors - self.__load_commands() # try to load the car's supported commands + check_voltage) # initialize by connecting and loading sensors + self.__load_commands() # try to load the car's supported commands logger.info("===================================================================") - def __connect(self, portstr, baudrate, protocol, check_voltage): """ Attempts to instantiate an ELM327 connection object. @@ -73,20 +72,20 @@ def __connect(self, portstr, baudrate, protocol, check_voltage): if portstr is None: logger.info("Using scan_serial to select port") - portnames = scan_serial() - logger.info("Available ports: " + str(portnames)) + port_names = scan_serial() + logger.info("Available ports: " + str(port_names)) - if not portnames: + if not port_names: logger.warning("No OBD-II adapters found") return - for port in portnames: + for port in port_names: logger.info("Attempting to use port: " + str(port)) self.interface = ELM327(port, baudrate, protocol, self.timeout, check_voltage) if self.interface.status() >= OBDStatus.ELM_CONNECTED: - break # success! stop searching for serial + break # success! stop searching for serial else: logger.info("Explicit port defined") self.interface = ELM327(portstr, baudrate, protocol, @@ -97,7 +96,6 @@ def __connect(self, portstr, baudrate, protocol, check_voltage): # the ELM327 class will report its own errors self.close() - def __load_commands(self): """ Queries for available PIDs, sets their support status, @@ -111,7 +109,7 @@ def __load_commands(self): logger.info("querying for supported commands") pid_getters = commands.pid_getters() for get in pid_getters: - # PID listing commands should sequentialy become supported + # PID listing commands should sequentially become supported # Mode 1 PID 0 is assumed to always be supported if not self.test_cmd(get, warn=False): continue @@ -124,12 +122,12 @@ def __load_commands(self): logger.info("No valid data for PID listing command: %s" % get) continue - # loop through PIDs bitarray + # loop through PIDs bit-array for i, bit in enumerate(response.value): if bit: mode = get.mode - pid = get.pid + i + 1 + pid = get.pid + i + 1 if commands.has_pid(mode, pid): self.supported_commands.add(commands[mode][pid]) @@ -140,7 +138,6 @@ def __load_commands(self): logger.info("finished querying with %d commands supported" % len(self.supported_commands)) - def __set_header(self, header): if header == self.__last_header: return @@ -148,12 +145,11 @@ def __set_header(self, header): if not r: logger.info("Set Header ('AT SH %s') did not return data", header) return OBDResponse() - if "\n".join([ m.raw() for m in r ]) != "OK": + if "\n".join([m.raw() for m in r]) != "OK": logger.info("Set Header ('AT SH %s') did not return 'OK'", header) return OBDResponse() self.__last_header = header - def close(self): """ Closes the connection, and clears supported_commands @@ -167,7 +163,6 @@ def close(self): self.interface.close() self.interface = None - def status(self): """ returns the OBD connection status """ if self.interface is None: @@ -175,7 +170,6 @@ def status(self): else: return self.interface.status() - # not sure how useful this would be # def ecus(self): @@ -185,7 +179,6 @@ def status(self): # else: # return self.interface.ecus() - def protocol_name(self): """ returns the name of the protocol being used by the ELM327 """ if self.interface is None: @@ -193,7 +186,6 @@ def protocol_name(self): else: return self.interface.protocol_name() - def protocol_id(self): """ returns the ID of the protocol being used by the ELM327 """ if self.interface is None: @@ -201,7 +193,6 @@ def protocol_id(self): else: return self.interface.protocol_id() - def port_name(self): """ Returns the name of the currently connected port """ if self.interface is not None: @@ -209,7 +200,6 @@ def port_name(self): else: return "" - def is_connected(self): """ Returns a boolean for whether a connection with the car was made. @@ -219,7 +209,6 @@ def is_connected(self): """ return self.status() == OBDStatus.CAR_CONNECTED - def print_commands(self): """ Utility function meant for working in interactive mode. @@ -228,7 +217,6 @@ def print_commands(self): for c in self.supported_commands: print(str(c)) - def supports(self, cmd): """ Returns a boolean for whether the given command @@ -236,7 +224,6 @@ def supports(self, cmd): """ return cmd in self.supported_commands - def test_cmd(self, cmd, warn=True): """ Returns a boolean for whether a command will @@ -256,7 +243,6 @@ def test_cmd(self, cmd, warn=True): return True - def query(self, cmd, force=False): """ primary API function. Sends commands to the car, and @@ -292,8 +278,7 @@ def query(self, cmd, force=False): logger.info("No valid OBD Messages returned") return OBDResponse() - return cmd(messages) # compute a response object - + return cmd(messages) # compute a response object def __build_command_string(self, cmd): """ assembles the appropriate command string """ diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index 93dd06cb..e677c03d 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -30,14 +30,13 @@ # # ######################################################################## +import logging from binascii import hexlify -from obd.utils import isHex, bitarray -import logging +from obd.utils import isHex, BitArray logger = logging.getLogger(__name__) - """ Basic data models for all protocols to use @@ -53,35 +52,37 @@ class ECU_HEADER: class ECU: """ constant flags used for marking and filtering messages """ - ALL = 0b11111111 # used by OBDCommands to accept messages from any ECU - ALL_KNOWN = 0b11111110 # used to ignore unknown ECUs, since this lib probably can't handle them + ALL = 0b11111111 # used by OBDCommands to accept messages from any ECU + ALL_KNOWN = 0b11111110 # used to ignore unknown ECUs, since this lib probably can't handle them # each ECU gets its own bit for ease of making OR filters - UNKNOWN = 0b00000001 # unknowns get their own bit, since they need to be accepted by the ALL filter - ENGINE = 0b00000010 + UNKNOWN = 0b00000001 # unknowns get their own bit, since they need to be accepted by the ALL filter + ENGINE = 0b00000010 TRANSMISSION = 0b00000100 class Frame(object): """ represents a single parsed line of OBD output """ + def __init__(self, raw): - self.raw = raw - self.data = bytearray() - self.priority = None + self.raw = raw + self.data = bytearray() + self.priority = None self.addr_mode = None - self.rx_id = None - self.tx_id = None - self.type = None - self.seq_index = 0 # only used when type = CF - self.data_len = None + self.rx_id = None + self.tx_id = None + self.type = None + self.seq_index = 0 # only used when type = CF + self.data_len = None class Message(object): """ represents a fully parsed OBD message of one or more Frames (lines) """ + def __init__(self, frames): self.frames = frames - self.ecu = ECU.UNKNOWN - self.data = bytearray() + self.ecu = ECU.UNKNOWN + self.data = bytearray() @property def tx_id(self): @@ -111,8 +112,6 @@ def __eq__(self, other): return False - - """ Protocol objects are factories for Frame and Message objects. They are @@ -124,18 +123,17 @@ def __eq__(self, other): """ -class Protocol(object): +class Protocol(object): # override in subclass for each protocol - ELM_NAME = "" # the ELM's name for this protocol (ie, "SAE J1939 (CAN 29/250)") - ELM_ID = "" # the ELM's ID for this protocol (ie, "A") + ELM_NAME = "" # the ELM's name for this protocol (ie, "SAE J1939 (CAN 29/250)") + ELM_ID = "" # the ELM's ID for this protocol (ie, "A") # the TX_IDs of known ECUs TX_ID_ENGINE = None TX_ID_TRANSMISSION = None - def __init__(self, lines_0100): """ constructs a protocol object @@ -158,11 +156,10 @@ def __init__(self, lines_0100): # log out the ecu map for tx_id, ecu in self.ecu_map.items(): - names = [k for k in ECU.__dict__ if ECU.__dict__[k] == ecu ] + names = [k for k, v in ECU.__dict__.items() if v == ecu] names = ", ".join(names) logger.debug("map ECU %d --> %s" % (tx_id, names)) - def __call__(self, lines): """ Main function @@ -185,7 +182,7 @@ def __call__(self, lines): if isHex(line_no_spaces): obd_lines.append(line_no_spaces) else: - non_obd_lines.append(line) # pass the original, un-scrubbed line + non_obd_lines.append(line) # pass the original, un-scrubbed line # ---------------------- handle valid OBD lines ---------------------- @@ -200,7 +197,6 @@ def __call__(self, lines): if self.parse_frame(frame): frames.append(frame) - # group frames by transmitting ECU # frames_by_ECU[tx_id] = [Frame, Frame] frames_by_ECU = {} @@ -229,11 +225,10 @@ def __call__(self, lines): for line in non_obd_lines: # give each line its own message object # messages are ECU.UNKNOWN by default - messages.append( Message([ Frame(line) ]) ) + messages.append(Message([Frame(line)])) return messages - def populate_ecu_map(self, messages): """ Given a list of messages from different ECUS, @@ -245,7 +240,7 @@ def populate_ecu_map(self, messages): # filter out messages that don't contain any data # this will prevent ELM responses from being mapped to ECUs - messages = [ m for m in messages if m.parsed() ] + messages = [m for m in messages if m.parsed()] # populate the map if len(messages) == 0: @@ -279,7 +274,7 @@ def populate_ecu_map(self, messages): tx_id = None for message in messages: - bits = bitarray(message.data).num_set() + bits = BitArray(message.data).num_set() if bits > best: best = bits @@ -292,7 +287,6 @@ def populate_ecu_map(self, messages): if m.tx_id not in self.ecu_map: self.ecu_map[m.tx_id] = ECU.UNKNOWN - def parse_frame(self, frame): """ override in subclass for each protocol @@ -305,7 +299,6 @@ def parse_frame(self, frame): """ raise NotImplementedError() - def parse_message(self, message): """ override in subclass for each protocol diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py index 2ed58644..ee2c3528 100644 --- a/obd/protocols/protocol_can.py +++ b/obd/protocols/protocol_can.py @@ -30,17 +30,16 @@ # # ######################################################################## +import logging from binascii import unhexlify -from obd.utils import contiguous -from .protocol import Protocol, Message, Frame, ECU -import logging +from obd.utils import contiguous +from .protocol import Protocol logger = logging.getLogger(__name__) class CANProtocol(Protocol): - TX_ID_ENGINE = 0 TX_ID_TRANSMISSION = 1 @@ -48,14 +47,12 @@ class CANProtocol(Protocol): FRAME_TYPE_FF = 0x10 # first frame of multi-frame message FRAME_TYPE_CF = 0x20 # consecutive frame(s) of multi-frame message - def __init__(self, lines_0100, id_bits): # this needs to be set FIRST, since the base # Protocol __init__ uses the parsing system. self.id_bits = id_bits Protocol.__init__(self, lines_0100) - def parse_frame(self, frame): raw = frame.raw @@ -92,7 +89,6 @@ def parse_frame(self, frame): logger.debug("Dropped frame for being too long") return False - # read header information if self.id_bits == 11: # Ex. @@ -103,29 +99,28 @@ def parse_frame(self, frame): frame.addr_mode = raw_bytes[3] & 0xF0 # 0xD0 = functional, 0xE0 = physical if frame.addr_mode == 0xD0: - #untested("11-bit functional request from tester") + # untested("11-bit functional request from tester") frame.rx_id = raw_bytes[3] & 0x0F # usually (always?) 0x0F for broadcast frame.tx_id = 0xF1 # made-up to mimic all other protocols elif raw_bytes[3] & 0x08: frame.rx_id = 0xF1 # made-up to mimic all other protocols frame.tx_id = raw_bytes[3] & 0x07 else: - #untested("11-bit message header from tester (functional or physical)") + # untested("11-bit message header from tester (functional or physical)") frame.tx_id = 0xF1 # made-up to mimic all other protocols frame.rx_id = raw_bytes[3] & 0x07 - else: # self.id_bits == 29: - frame.priority = raw_bytes[0] # usually (always?) 0x18 + else: # self.id_bits == 29: + frame.priority = raw_bytes[0] # usually (always?) 0x18 frame.addr_mode = raw_bytes[1] # DB = functional, DA = physical - frame.rx_id = raw_bytes[2] # 0x33 = broadcast (functional) - frame.tx_id = raw_bytes[3] # 0xF1 = tester ID + frame.rx_id = raw_bytes[2] # 0x33 = broadcast (functional) + frame.tx_id = raw_bytes[3] # 0xF1 = tester ID # extract the frame data # [ Frame ] # 00 00 07 E8 06 41 00 BE 7F B8 13 frame.data = raw_bytes[4:] - # read PCI byte (always first byte in the data section) # v # 00 00 07 E8 06 41 00 BE 7F B8 13 @@ -136,7 +131,6 @@ def parse_frame(self, frame): logger.debug("Dropping frame carrying unknown PCI frame type") return False - if frame.type == self.FRAME_TYPE_SF: # single frames have 4 bit length codes # v @@ -166,7 +160,6 @@ def parse_frame(self, frame): return True - def parse_message(self, message): frames = message.frames @@ -182,7 +175,7 @@ def parse_message(self, message): # [ Frame ] # [ Data ] # 00 00 07 E8 06 41 00 BE 7F B8 13 xx xx xx xx, anything else is ignored - message.data = frame.data[1:1+frame.data_len] + message.data = frame.data[1:1 + frame.data_len] else: # sort FF and CF into their own lists @@ -216,7 +209,7 @@ def parse_message(self, message): # Frame sequence numbers only specify the low order bits, so compute the # full sequence number from the frame number and the last sequence number seen: # 1) take the high order bits from the last_sn and low order bits from the frame - seq = (prev.seq_index & ~0x0F) + (curr.seq_index) + seq = (prev.seq_index & ~0x0F) + curr.seq_index # 2) if this is more than 7 frames away, we probably just wrapped (e.g., # last=0x0F current=0x01 should mean 0x11, not 0x01) if seq < prev.seq_index - 7: @@ -234,14 +227,12 @@ def parse_message(self, message): logger.debug("Recieved multiline response with missing frames") return False - # first frame: # [ Frame ] # [PCI] <-- first frame has a 2 byte PCI # [L ] [ Data ] L = length of message in bytes # 00 00 07 E8 10 13 49 04 01 35 36 30 - # consecutive frame: # [ Frame ] # [] <-- consecutive frames have a 1 byte PCI @@ -249,23 +240,20 @@ def parse_message(self, message): # 00 00 07 E8 21 32 38 39 34 39 41 43 # 00 00 07 E8 22 00 00 00 00 00 00 31 - # original data: # [ specified message length (from first-frame) ] # 49 04 01 35 36 30 32 38 39 34 39 41 43 00 00 00 00 00 00 31 - # on the first frame, skip PCI byte AND length code message.data = ff[0].data[2:] # now that they're in order, load/accumulate the data from each CF frame for f in cf: - message.data += f.data[1:] # chop off the PCI byte + message.data += f.data[1:] # chop off the PCI byte # chop to the correct size (as specified in the first frame) message.data = message.data[:ff[0].data_len] - # trim DTC requests based on DTC count # this ISN'T in the decoder because the legacy protocols # don't provide a DTC_count bytes, and instead, insert a 0x00 @@ -276,8 +264,8 @@ def parse_message(self, message): # 43 03 11 11 22 22 33 33 # [DTC] [DTC] [DTC] - num_dtc_bytes = message.data[1] * 2 # each DTC is 2 bytes - message.data = message.data[:(num_dtc_bytes + 2)] # add 2 to account for mode/DTC_count bytes + num_dtc_bytes = message.data[1] * 2 # each DTC is 2 bytes + message.data = message.data[:(num_dtc_bytes + 2)] # add 2 to account for mode/DTC_count bytes return True @@ -289,10 +277,10 @@ def parse_message(self, message): ############################################## - class ISO_15765_4_11bit_500k(CANProtocol): ELM_NAME = "ISO 15765-4 (CAN 11/500)" ELM_ID = "6" + def __init__(self, lines_0100): CANProtocol.__init__(self, lines_0100, id_bits=11) @@ -300,6 +288,7 @@ def __init__(self, lines_0100): class ISO_15765_4_29bit_500k(CANProtocol): ELM_NAME = "ISO 15765-4 (CAN 29/500)" ELM_ID = "7" + def __init__(self, lines_0100): CANProtocol.__init__(self, lines_0100, id_bits=29) @@ -307,6 +296,7 @@ def __init__(self, lines_0100): class ISO_15765_4_11bit_250k(CANProtocol): ELM_NAME = "ISO 15765-4 (CAN 11/250)" ELM_ID = "8" + def __init__(self, lines_0100): CANProtocol.__init__(self, lines_0100, id_bits=11) @@ -314,6 +304,7 @@ def __init__(self, lines_0100): class ISO_15765_4_29bit_250k(CANProtocol): ELM_NAME = "ISO 15765-4 (CAN 29/250)" ELM_ID = "9" + def __init__(self, lines_0100): CANProtocol.__init__(self, lines_0100, id_bits=29) @@ -321,5 +312,6 @@ def __init__(self, lines_0100): class SAE_J1939(CANProtocol): ELM_NAME = "SAE J1939 (CAN 29/250)" ELM_ID = "A" + def __init__(self, lines_0100): CANProtocol.__init__(self, lines_0100, id_bits=29) diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py index d7d3b0e5..ca393769 100644 --- a/obd/protocols/protocol_legacy.py +++ b/obd/protocols/protocol_legacy.py @@ -30,24 +30,21 @@ # # ######################################################################## +import logging from binascii import unhexlify -from obd.utils import contiguous -from .protocol import Protocol, Message, Frame, ECU -import logging +from obd.utils import contiguous +from .protocol import Protocol logger = logging.getLogger(__name__) class LegacyProtocol(Protocol): - TX_ID_ENGINE = 0x10 - def __init__(self, lines_0100): Protocol.__init__(self, lines_0100) - def parse_frame(self, frame): raw = frame.raw @@ -77,12 +74,11 @@ def parse_frame(self, frame): # read header information frame.priority = raw_bytes[0] - frame.rx_id = raw_bytes[1] - frame.tx_id = raw_bytes[2] + frame.rx_id = raw_bytes[1] + frame.tx_id = raw_bytes[2] return True - def parse_message(self, message): frames = message.frames @@ -117,7 +113,7 @@ def parse_message(self, message): # 48 6B 10 43 03 04 00 00 00 00 ck # [ Data ] - message.data = bytearray([0x43, 0x00]) # forge the mode byte and CAN's DTC_count byte + message.data = bytearray([0x43, 0x00]) # forge the mode byte and CAN's DTC_count byte for f in frames: message.data += f.data[1:] @@ -131,7 +127,7 @@ def parse_message(self, message): message.data = frames[0].data - else: # len(frames) > 1: + else: # len(frames) > 1: # generic multiline requests carry an order byte # Ex. @@ -158,17 +154,16 @@ def parse_message(self, message): # now that they're in order, accumulate the data from each frame # preserve the first frame's mode and PID bytes (for consistency with CAN) - frames[0].data.pop(2) # remove the sequence byte + frames[0].data.pop(2) # remove the sequence byte message.data = frames[0].data # add the data from the remaining frames for f in frames[1:]: - message.data += f.data[3:] # loose the mode/pid/seq bytes + message.data += f.data[3:] # loose the mode/pid/seq bytes return True - ############################################## # # # Here lie the class stubs for each protocol # @@ -176,37 +171,26 @@ def parse_message(self, message): ############################################## - class SAE_J1850_PWM(LegacyProtocol): ELM_NAME = "SAE J1850 PWM" ELM_ID = "1" - def __init__(self, lines_0100): - LegacyProtocol.__init__(self, lines_0100) class SAE_J1850_VPW(LegacyProtocol): ELM_NAME = "SAE J1850 VPW" ELM_ID = "2" - def __init__(self, lines_0100): - LegacyProtocol.__init__(self, lines_0100) class ISO_9141_2(LegacyProtocol): ELM_NAME = "ISO 9141-2" ELM_ID = "3" - def __init__(self, lines_0100): - LegacyProtocol.__init__(self, lines_0100) class ISO_14230_4_5baud(LegacyProtocol): ELM_NAME = "ISO 14230-4 (KWP 5BAUD)" ELM_ID = "4" - def __init__(self, lines_0100): - LegacyProtocol.__init__(self, lines_0100) class ISO_14230_4_fast(LegacyProtocol): ELM_NAME = "ISO 14230-4 (KWP FAST)" ELM_ID = "5" - def __init__(self, lines_0100): - LegacyProtocol.__init__(self, lines_0100) diff --git a/obd/protocols/protocol_unknown.py b/obd/protocols/protocol_unknown.py index 0a94fadb..c120b7e6 100644 --- a/obd/protocols/protocol_unknown.py +++ b/obd/protocols/protocol_unknown.py @@ -35,7 +35,6 @@ class UnknownProtocol(Protocol): - """ Class representing an unknown protocol. @@ -44,7 +43,7 @@ class UnknownProtocol(Protocol): """ def parse_frame(self, frame): - return True # pass everything + return True # pass everything def parse_message(self, message): - return True # pass everything + return True # pass everything diff --git a/obd/utils.py b/obd/utils.py index 679c27ea..77cb36e2 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -30,15 +30,15 @@ # # ######################################################################## -import serial import errno -import string import glob -import sys import logging +import string +import sys -logger = logging.getLogger(__name__) +import serial +logger = logging.getLogger(__name__) class OBDStatus: @@ -50,9 +50,7 @@ class OBDStatus: CAR_CONNECTED = "Car Connected" - - -class bitarray: +class BitArray: """ Class for representing bitarrays (inefficiently) @@ -65,7 +63,7 @@ def __init__(self, _bytearray): self.bits = "" for b in _bytearray: v = bin(b)[2:] - self.bits += ("0" * (8 - len(v))) + v # pad it with zeros + self.bits += ("0" * (8 - len(v))) + v # pad it with zeros def __getitem__(self, key): if isinstance(key, int): @@ -76,7 +74,7 @@ def __getitem__(self, key): elif isinstance(key, slice): bits = self.bits[key] if bits: - return [ b == "1" for b in bits ] + return [b == "1" for b in bits] else: return [] @@ -100,7 +98,7 @@ def __str__(self): return self.bits def __iter__(self): - return [ b == "1" for b in self.bits ].__iter__() + return [b == "1" for b in self.bits].__iter__() def bytes_to_int(bs): @@ -108,10 +106,11 @@ def bytes_to_int(bs): v = 0 p = 0 for b in reversed(bs): - v += b * (2**p) + v += b * (2 ** p) p += 8 return v + def bytes_to_hex(bs): h = "" for b in bs: @@ -119,15 +118,18 @@ def bytes_to_hex(bs): h += ("0" * (2 - len(bh))) + bh return h + def twos_comp(val, num_bits): """compute the 2's compliment of int value val""" - if( (val&(1<<(num_bits-1))) != 0 ): - val = val - (1< Date: Wed, 27 Feb 2019 22:32:11 -0800 Subject: [PATCH 524/569] tests: Fix incorrect whitespace according to PEP8 rules Signed-off-by: Alistair Francis --- tests/conftest.py | 6 +- tests/test_OBD.py | 59 ++++---- tests/test_OBDCommand.py | 43 +++--- tests/test_commands.py | 63 ++++---- tests/test_decoders.py | 271 +++++++++++++++++++--------------- tests/test_obdsim.py | 81 +++++----- tests/test_protocol.py | 23 ++- tests/test_protocol_can.py | 46 +++--- tests/test_protocol_legacy.py | 51 +++---- tests/test_uas.py | 115 +++++++++++++-- 10 files changed, 416 insertions(+), 342 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b4216842..630d9044 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,2 @@ - -import pytest - def pytest_addoption(parser): - parser.addoption("--port", action="store", default=None, - help="device file for doing end-to-end testing") + parser.addoption("--port", action="store", help="device file for doing end-to-end testing") diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 81516f2f..3c9bfb4a 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -1,16 +1,13 @@ - """ Tests for the API layer """ import obd -from obd import Unit from obd import ECU -from obd.protocols.protocol import Message -from obd.utils import OBDStatus from obd.OBDCommand import OBDCommand from obd.decoders import noop - +from obd.protocols.protocol import Message +from obd.utils import OBDStatus class FakeELM: @@ -18,24 +15,27 @@ class FakeELM: Fake ELM327 driver class for intercepting the commands from the API """ - def __init__(self, portname, UNUSED_baudrate=None, UNUSED_protocol=None): - self._portname = portname + def __init__(self, port_name): + self._port_name = port_name self._status = OBDStatus.CAR_CONNECTED self._last_command = None def port_name(self): - return self._portname + return self._port_name def status(self): return self._status - def ecus(self): - return [ ECU.ENGINE, ECU.UNKNOWN ] + @staticmethod + def ecus(): + return [ECU.ENGINE, ECU.UNKNOWN] - def protocol_name(self): + @staticmethod + def protocol_name(): return "ISO 15765-4 (CAN 11/500)" - def protocol_id(self): + @staticmethod + def protocol_id(): return "6" def close(self): @@ -49,8 +49,8 @@ def send_and_parse(self, cmd): # all commands succeed message = Message([]) message.data = bytearray(b'response data') - message.ecu = ECU.ENGINE # picked engine so that simple commands like RPM will work - return [ message ] + message.ecu = ECU.ENGINE # picked engine so that simple commands like RPM will work + return [message] def _test_last_command(self, expected): r = self._last_command == expected @@ -59,18 +59,15 @@ def _test_last_command(self, expected): # a toy command to test with -command = OBDCommand("Test_Command", \ - "A test command", \ - "0123456789ABCDEF", \ - 0, \ - noop, \ - ECU.ALL, \ +command = OBDCommand("Test_Command", + "A test command", + "0123456789ABCDEF", + 0, + noop, + ECU.ALL, True) - - - def test_is_connected(): o = obd.OBD("/dev/null") assert not o.is_connected() @@ -122,10 +119,10 @@ def test_port_name(): """ o = obd.OBD("/dev/null") o.interface = FakeELM("/dev/null") - assert o.port_name() == o.interface._portname + assert o.port_name() == o.interface._port_name o.interface = FakeELM("A different port name") - assert o.port_name() == o.interface._portname + assert o.port_name() == o.interface._port_name def test_protocol_name(): @@ -148,16 +145,13 @@ def test_protocol_id(): assert o.protocol_id() == o.interface.protocol_id() - - - - """ The following tests are for the query() function """ + def test_force(): - o = obd.OBD("/dev/null", fast=False) # disable the trailing response count byte + o = obd.OBD("/dev/null", fast=False) # disable the trailing response count byte o.interface = FakeELM("/dev/null") r = o.query(obd.commands.RPM) @@ -173,15 +167,14 @@ def test_force(): assert r.is_null() assert o.interface._test_last_command(None) - r = o.query(command, force=True) + o.query(command, force=True) assert o.interface._test_last_command(command.command) - def test_fast(): o = obd.OBD("/dev/null", fast=False) o.interface = FakeELM("/dev/null") assert command.fast - o.query(command, force=True) # force since this command isn't in the tables + o.query(command, force=True) # force since this command isn't in the tables # assert o.interface._test_last_command(command.command) diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py index f95b4d36..ac8bbafe 100644 --- a/tests/test_OBDCommand.py +++ b/tests/test_OBDCommand.py @@ -1,32 +1,27 @@ - from obd.OBDCommand import OBDCommand -from obd.UnitsAndScaling import Unit from obd.decoders import noop from obd.protocols import * - def test_constructor(): - # default constructor # name description cmd bytes decoder ECU cmd = OBDCommand("Test", "example OBD command", b"0123", 2, noop, ECU.ENGINE) - assert cmd.name == "Test" - assert cmd.desc == "example OBD command" - assert cmd.command == b"0123" - assert cmd.bytes == 2 - assert cmd.decode == noop - assert cmd.ecu == ECU.ENGINE - assert cmd.fast == False + assert cmd.name is "Test" + assert cmd.desc is "example OBD command" + assert cmd.command == b"0123" + assert cmd.bytes == 2 + assert cmd.decode == noop + assert cmd.ecu == ECU.ENGINE + assert cmd.fast is False assert cmd.mode == 1 - assert cmd.pid == 35 + assert cmd.pid == 35 # a case where "fast", and "supported" were set explicitly # name description cmd bytes decoder ECU fast cmd = OBDCommand("Test 2", "example OBD command", b"0123", 2, noop, ECU.ENGINE, True) - assert cmd.fast == True - + assert cmd.fast is True def test_clone(): @@ -34,19 +29,18 @@ def test_clone(): cmd = OBDCommand("", "", b"0123", 2, noop, ECU.ENGINE) other = cmd.clone() - assert cmd.name == other.name - assert cmd.desc == other.desc - assert cmd.command == other.command - assert cmd.bytes == other.bytes - assert cmd.decode == other.decode - assert cmd.ecu == other.ecu - assert cmd.fast == cmd.fast - + assert cmd.name == other.name + assert cmd.desc == other.desc + assert cmd.command == other.command + assert cmd.bytes == other.bytes + assert cmd.decode == other.decode + assert cmd.ecu == other.ecu + assert cmd.fast == cmd.fast def test_call(): - p = SAE_J1850_PWM(["48 6B 10 41 00 FF FF FF FF AA"]) # train the ecu_map to identify the engine - messages = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) # parse valid data into response object + p = SAE_J1850_PWM(["48 6B 10 41 00 FF FF FF FF AA"]) # train the ecu_map to identify the engine + messages = p(["48 6B 10 41 00 BE 1F B8 11 AA"]) # parse valid data into response object print(messages[0].data) @@ -66,7 +60,6 @@ def test_call(): assert r.value == bytearray([0x41, 0x00, 0xBE, 0x1F, 0xB8]) - def test_get_mode(): cmd = OBDCommand("", "", b"0123", 4, noop, ECU.ENGINE) assert cmd.mode == 0x01 diff --git a/tests/test_commands.py b/tests/test_commands.py index 3d6c033e..1cbd71d4 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,34 +1,33 @@ - import obd from obd.decoders import pid def test_list_integrity(): - for mode, cmds in enumerate(obd.commands.modes): - for pid, cmd in enumerate(cmds): + for mode, command_list in enumerate(obd.commands.modes): + for pid_index, cmd in enumerate(command_list): if cmd is None: - continue # this command is reserved + continue # this command is reserved - assert cmd.command != b"", "The Command's command string must not be null" + assert cmd.command != b"", "The Command's command string must not be null" # make sure the command tables are in mode & PID order - assert mode == cmd.mode, "Command is in the wrong mode list: %s" % cmd.name + assert mode == cmd.mode, "Command is in the wrong mode list: %s" % cmd.name - if len(cmds) > 1: - assert pid == cmd.pid, "The index in the list must also be the PID: %s" % cmd.name + if len(command_list) > 1: + assert pid_index == cmd.pid, "The index in the list must also be the PID: %s" % cmd.name else: # lone commands in a mode are allowed to have no PID - assert (pid == cmd.pid) or (cmd.pid is None) + assert (pid_index == cmd.pid) or (cmd.pid is None) # make sure all the fields are set - assert cmd.name != "", "Command names must not be null" - assert cmd.name.isupper(), "Command names must be upper case" - assert ' ' not in cmd.name, "No spaces allowed in command names" - assert cmd.desc != "", "Command description must not be null" - assert (mode >= 1) and (mode <= 9), "Mode must be in the range [1, 9] (decimal)" - assert (pid >= 0) and (pid <= 196), "PID must be in the range [0, 196] (decimal)" - assert cmd.bytes >= 0, "Number of return bytes must be >= 0" + assert cmd.name != "", "Command names must not be null" + assert cmd.name.isupper(), "Command names must be upper case" + assert ' ' not in cmd.name, "No spaces allowed in command names" + assert cmd.desc != "", "Command description must not be null" + assert (mode >= 1) and (mode <= 9), "Mode must be in the range [1, 9] (decimal)" + assert (pid_index >= 0) and (pid_index <= 196), "PID must be in the range [0, 196] (decimal)" + assert cmd.bytes >= 0, "Number of return bytes must be >= 0" assert hasattr(cmd.decode, '__call__'), "Decode is not callable" @@ -36,11 +35,11 @@ def test_unique_names(): # make sure no two commands have the same name names = {} - for cmds in obd.commands.modes: - for cmd in cmds: + for command_list in obd.commands.modes: + for cmd in command_list: if cmd is None: - continue # this command is reserved + continue # this command is reserved assert not names.__contains__(cmd.name), "Two commands share the same name: %s" % cmd.name names[cmd.name] = True @@ -48,31 +47,33 @@ def test_unique_names(): def test_getitem(): # ensure that __getitem__ works correctly - for cmds in obd.commands.modes: - for cmd in cmds: + for command_list in obd.commands.modes: + for cmd in command_list: if cmd is None: - continue # this command is reserved + continue # this command is reserved # by [mode][pid] - if (cmd.pid is None) and (len(cmds) == 1): + if (cmd.pid is None) and (len(command_list) == 1): # lone commands in a mode have no PID, and report a pid # value of None, but can still be accessed by PID 0 - assert cmd == obd.commands[cmd.mode][0], "lone command in mode %d could not be accessed through __getitem__" % mode + error_msg = "lone command in mode %d could not be accessed through __getitem__" % cmd.mode + assert cmd == obd.commands[cmd.mode][0], error_msg else: - assert cmd == obd.commands[cmd.mode][cmd.pid], "mode %d, PID %d could not be accessed through __getitem__" % (mode, pid) + error_msg = "mode %d, PID %d could not be accessed through __getitem__" % (cmd.mode, cmd.pid) + assert cmd == obd.commands[cmd.mode][cmd.pid], error_msg # by [name] - assert cmd == obd.commands[cmd.name], "command name %s could not be accessed through __getitem__" % (cmd.name) + assert cmd == obd.commands[cmd.name], "command name %s could not be accessed through __getitem__" % ( + cmd.name) def test_contains(): - - for cmds in obd.commands.modes: - for cmd in cmds: + for command_list in obd.commands.modes: + for cmd in command_list: if cmd is None: - continue # this command is reserved + continue # this command is reserved # by (command) assert obd.commands.has_command(cmd) @@ -105,7 +106,7 @@ def test_pid_getters(): for cmd in mode: if cmd is None: - continue # this command is reserved + continue # this command is reserved if cmd.decode == pid: assert cmd in pid_getters diff --git a/tests/test_decoders.py b/tests/test_decoders.py index f87081e9..9f49b062 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -1,23 +1,23 @@ - from binascii import unhexlify +import obd.decoders as d from obd.UnitsAndScaling import Unit -from obd.protocols.protocol import Frame, Message from obd.codes import BASE_TESTS, COMPRESSION_TESTS, SPARK_TESTS, TEST_IDS -import obd.decoders as d +from obd.protocols.protocol import Frame, Message # returns a list with a single valid message, # containing the requested data -def m(hex_data, frames=[]): +def m(hex_data, frames=None): # most decoders don't look at the underlying frame objects - message = Message(frames) + message = Message(frames or []) message.data = bytearray(unhexlify(hex_data)) return [message] FLOAT_EQUALS_TOLERANCE = 0.025 + # comparison for pint floating point values def float_equals(va, vb): units_match = (va.u == vb.u) @@ -31,143 +31,175 @@ def float_equals(va, vb): def test_noop(): assert d.noop(m("00010203")) == bytearray([0, 1, 2, 3]) + def test_drop(): - assert d.drop(m("deadbeef")) == None + assert d.drop(m("deadbeef")) is None + def test_raw_string(): - assert d.raw_string([ Message([]) ]) == "" - assert d.raw_string([ Message([ Frame("NO DATA") ]) ]) == "NO DATA" - assert d.raw_string([ Message([ Frame("A"), Frame("B") ]) ]) == "A\nB" - assert d.raw_string([ Message([ Frame("A") ]), Message([ Frame("B") ]) ]) == "A\nB" + assert d.raw_string([Message([])]) == "" + assert d.raw_string([Message([Frame("NO DATA")])]) == "NO DATA" + assert d.raw_string([Message([Frame("A"), Frame("B")])]) == "A\nB" + assert d.raw_string([Message([Frame("A")]), Message([Frame("B")])]) == "A\nB" + def test_pid(): - assert d.pid(m("4100"+"00000000")).bits == "00000000000000000000000000000000" - assert d.pid(m("4100"+"F00AA00F")).bits == "11110000000010101010000000001111" - assert d.pid(m("4100"+"11")).bits == "00010001" + assert d.pid(m("4100" + "00000000")).bits == "00000000000000000000000000000000" + assert d.pid(m("4100" + "F00AA00F")).bits == "11110000000010101010000000001111" + assert d.pid(m("4100" + "11")).bits == "00010001" + def test_percent(): - assert d.percent(m("4100"+"00")) == 0.0 * Unit.percent - assert d.percent(m("4100"+"FF")) == 100.0 * Unit.percent + assert d.percent(m("4100" + "00")) == 0.0 * Unit.percent + assert d.percent(m("4100" + "FF")) == 100.0 * Unit.percent + def test_percent_centered(): - assert d.percent_centered(m("4100"+"00")) == -100.0 * Unit.percent - assert d.percent_centered(m("4100"+"80")) == 0.0 * Unit.percent - assert float_equals(d.percent_centered(m("4100"+"FF")), 99.2 * Unit.percent) + assert d.percent_centered(m("4100" + "00")) == -100.0 * Unit.percent + assert d.percent_centered(m("4100" + "80")) == 0.0 * Unit.percent + assert float_equals(d.percent_centered(m("4100" + "FF")), 99.2 * Unit.percent) + def test_temp(): - assert d.temp(m("4100"+"00")) == Unit.Quantity(-40, Unit.celsius) - assert d.temp(m("4100"+"FF")) == Unit.Quantity(215, Unit.celsius) - assert d.temp(m("4100"+"03E8")) == Unit.Quantity(960, Unit.celsius) + assert d.temp(m("4100" + "00")) == Unit.Quantity(-40, Unit.celsius) + assert d.temp(m("4100" + "FF")) == Unit.Quantity(215, Unit.celsius) + assert d.temp(m("4100" + "03E8")) == Unit.Quantity(960, Unit.celsius) + def test_current_centered(): - assert d.current_centered(m("4100"+"00000000")) == -128.0 * Unit.milliampere - assert d.current_centered(m("4100"+"00008000")) == 0.0 * Unit.milliampere - assert d.current_centered(m("4100"+"ABCD8000")) == 0.0 * Unit.milliampere # first 2 bytes are unused (should be disregarded) - assert float_equals(d.current_centered(m("4100"+"0000FFFF")), 128.0 * Unit.milliampere) + assert d.current_centered(m("4100" + "00000000")) == -128.0 * Unit.milliampere + assert d.current_centered(m("4100" + "00008000")) == 0.0 * Unit.milliampere + assert d.current_centered(m("4100" + "ABCD8000")) == 0.0 * Unit.milliampere + # first 2 bytes are unused (should be disregarded) + assert float_equals(d.current_centered(m("4100" + "0000FFFF")), 128.0 * Unit.milliampere) + def test_sensor_voltage(): - assert d.sensor_voltage(m("4100"+"0000")) == 0.0 * Unit.volt - assert d.sensor_voltage(m("4100"+"FFFF")) == 1.275 * Unit.volt + assert d.sensor_voltage(m("4100" + "0000")) == 0.0 * Unit.volt + assert d.sensor_voltage(m("4100" + "FFFF")) == 1.275 * Unit.volt + def test_sensor_voltage_big(): - assert d.sensor_voltage_big(m("4100"+"00000000")) == 0.0 * Unit.volt - assert float_equals(d.sensor_voltage_big(m("4100"+"00008000")), 4.0 * Unit.volt) - assert d.sensor_voltage_big(m("4100"+"0000FFFF")) == 8.0 * Unit.volt - assert d.sensor_voltage_big(m("4100"+"ABCD0000")) == 0.0 * Unit.volt # first 2 bytes are unused (should be disregarded) + assert d.sensor_voltage_big(m("4100" + "00000000")) == 0.0 * Unit.volt + assert float_equals(d.sensor_voltage_big(m("4100" + "00008000")), 4.0 * Unit.volt) + assert d.sensor_voltage_big(m("4100" + "0000FFFF")) == 8.0 * Unit.volt + assert d.sensor_voltage_big(m("4100" + "ABCD0000")) == 0.0 * Unit.volt + # first 2 bytes are unused (should be disregarded) + def test_fuel_pressure(): - assert d.fuel_pressure(m("4100"+"00")) == 0 * Unit.kilopascal - assert d.fuel_pressure(m("4100"+"80")) == 384 * Unit.kilopascal - assert d.fuel_pressure(m("4100"+"FF")) == 765 * Unit.kilopascal + assert d.fuel_pressure(m("4100" + "00")) == 0 * Unit.kilopascal + assert d.fuel_pressure(m("4100" + "80")) == 384 * Unit.kilopascal + assert d.fuel_pressure(m("4100" + "FF")) == 765 * Unit.kilopascal + def test_pressure(): - assert d.pressure(m("4100"+"00")) == 0 * Unit.kilopascal - assert d.pressure(m("4100"+"00")) == 0 * Unit.kilopascal + assert d.pressure(m("4100" + "00")) == 0 * Unit.kilopascal + assert d.pressure(m("4100" + "00")) == 0 * Unit.kilopascal + def test_evap_pressure(): - pass # TODO - #assert d.evap_pressure(m("4100"+"0000")) == 0.0 * Unit.PA) + pass + # TODO + # assert d.evap_pressure(m("4100"+"0000")) == 0.0 * Unit.PA) + def test_abs_evap_pressure(): - assert d.abs_evap_pressure(m("4100"+"0000")) == 0 * Unit.kilopascal - assert d.abs_evap_pressure(m("4100"+"FFFF")) == 327.675 * Unit.kilopascal + assert d.abs_evap_pressure(m("4100" + "0000")) == 0 * Unit.kilopascal + assert d.abs_evap_pressure(m("4100" + "FFFF")) == 327.675 * Unit.kilopascal + def test_evap_pressure_alt(): - assert d.evap_pressure_alt(m("4100"+"0000")) == -32767 * Unit.pascal - assert d.evap_pressure_alt(m("4100"+"7FFF")) == 0 * Unit.pascal - assert d.evap_pressure_alt(m("4100"+"FFFF")) == 32768 * Unit.pascal + assert d.evap_pressure_alt(m("4100" + "0000")) == -32767 * Unit.pascal + assert d.evap_pressure_alt(m("4100" + "7FFF")) == 0 * Unit.pascal + assert d.evap_pressure_alt(m("4100" + "FFFF")) == 32768 * Unit.pascal + def test_timing_advance(): - assert d.timing_advance(m("4100"+"00")) == -64.0 * Unit.degrees - assert d.timing_advance(m("4100"+"FF")) == 63.5 * Unit.degrees + assert d.timing_advance(m("4100" + "00")) == -64.0 * Unit.degrees + assert d.timing_advance(m("4100" + "FF")) == 63.5 * Unit.degrees + def test_inject_timing(): - assert d.inject_timing(m("4100"+"0000")) == -210 * Unit.degrees - assert float_equals(d.inject_timing(m("4100"+"FFFF")), 302 * Unit.degrees) + assert d.inject_timing(m("4100" + "0000")) == -210 * Unit.degrees + assert float_equals(d.inject_timing(m("4100" + "FFFF")), 302 * Unit.degrees) + def test_max_maf(): - assert d.max_maf(m("4100"+"00000000")) == 0 * Unit.grams_per_second - assert d.max_maf(m("4100"+"FF000000")) == 2550 * Unit.grams_per_second - assert d.max_maf(m("4100"+"00ABCDEF")) == 0 * Unit.grams_per_second # last 3 bytes are unused (should be disregarded) + assert d.max_maf(m("4100" + "00000000")) == 0 * Unit.grams_per_second + assert d.max_maf(m("4100" + "FF000000")) == 2550 * Unit.grams_per_second + assert d.max_maf( + m("4100" + "00ABCDEF")) == 0 * Unit.grams_per_second # last 3 bytes are unused (should be disregarded) + def test_fuel_rate(): - assert d.fuel_rate(m("4100"+"0000")) == 0.0 * Unit.liters_per_hour - assert d.fuel_rate(m("4100"+"FFFF")) == 3276.75 * Unit.liters_per_hour + assert d.fuel_rate(m("4100" + "0000")) == 0.0 * Unit.liters_per_hour + assert d.fuel_rate(m("4100" + "FFFF")) == 3276.75 * Unit.liters_per_hour + def test_fuel_status(): - assert d.fuel_status(m("4100"+"0100")) == ("Open loop due to insufficient engine temperature", "") - assert d.fuel_status(m("4100"+"0800")) == ("Open loop due to system failure", "") - assert d.fuel_status(m("4100"+"0808")) == ("Open loop due to system failure", - "Open loop due to system failure") - assert d.fuel_status(m("4100"+"0008")) == ("", "Open loop due to system failure") - assert d.fuel_status(m("4100"+"0000")) == None - assert d.fuel_status(m("4100"+"0300")) == None - assert d.fuel_status(m("4100"+"0303")) == None + assert d.fuel_status(m("4100" + "0100")) == ("Open loop due to insufficient engine temperature", "") + assert d.fuel_status(m("4100" + "0800")) == ("Open loop due to system failure", "") + assert d.fuel_status(m("4100" + "0808")) == ("Open loop due to system failure", + "Open loop due to system failure") + assert d.fuel_status(m("4100" + "0008")) == ("", "Open loop due to system failure") + assert d.fuel_status(m("4100" + "0000")) is None + assert d.fuel_status(m("4100" + "0300")) is None + assert d.fuel_status(m("4100" + "0303")) is None + def test_air_status(): - assert d.air_status(m("4100"+"01")) == "Upstream" - assert d.air_status(m("4100"+"08")) == "Pump commanded on for diagnostics" - assert d.air_status(m("4100"+"03")) == None + assert d.air_status(m("4100" + "01")) == "Upstream" + assert d.air_status(m("4100" + "08")) == "Pump commanded on for diagnostics" + assert d.air_status(m("4100" + "03")) is None + def test_fuel_type(): - assert d.fuel_type(m("4100"+"00")) == "Not available" - assert d.fuel_type(m("4100"+"17")) == "Bifuel running diesel" - assert d.fuel_type(m("4100"+"18")) == None + assert d.fuel_type(m("4100" + "00")) == "Not available" + assert d.fuel_type(m("4100" + "17")) == "Bifuel running diesel" + assert d.fuel_type(m("4100" + "18")) is None + def test_obd_compliance(): - assert d.obd_compliance(m("4100"+"00")) == "Undefined" - assert d.obd_compliance(m("4100"+"21")) == "Heavy Duty Euro OBD Stage VI (HD EOBD-IV)" - assert d.obd_compliance(m("4100"+"22")) == None + assert d.obd_compliance(m("4100" + "00")) == "Undefined" + assert d.obd_compliance(m("4100" + "21")) == "Heavy Duty Euro OBD Stage VI (HD EOBD-IV)" + assert d.obd_compliance(m("4100" + "22")) is None + def test_o2_sensors(): - assert d.o2_sensors(m("4100"+"00")) == ((),(False, False, False, False), (False, False, False, False)) - assert d.o2_sensors(m("4100"+"01")) == ((),(False, False, False, False), (False, False, False, True)) - assert d.o2_sensors(m("4100"+"0F")) == ((),(False, False, False, False), (True, True, True, True)) - assert d.o2_sensors(m("4100"+"F0")) == ((),(True, True, True, True), (False, False, False, False)) + assert d.o2_sensors(m("4100" + "00")) == ((), (False, False, False, False), (False, False, False, False)) + assert d.o2_sensors(m("4100" + "01")) == ((), (False, False, False, False), (False, False, False, True)) + assert d.o2_sensors(m("4100" + "0F")) == ((), (False, False, False, False), (True, True, True, True)) + assert d.o2_sensors(m("4100" + "F0")) == ((), (True, True, True, True), (False, False, False, False)) + def test_o2_sensors_alt(): - assert d.o2_sensors_alt(m("4100"+"00")) == ((),(False, False), (False, False), (False, False), (False, False)) - assert d.o2_sensors_alt(m("4100"+"01")) == ((),(False, False), (False, False), (False, False), (False, True)) - assert d.o2_sensors_alt(m("4100"+"0F")) == ((),(False, False), (False, False), (True, True), (True, True)) - assert d.o2_sensors_alt(m("4100"+"F0")) == ((),(True, True), (True, True), (False, False), (False, False)) + assert d.o2_sensors_alt(m("4100" + "00")) == ((), (False, False), (False, False), (False, False), (False, False)) + assert d.o2_sensors_alt(m("4100" + "01")) == ((), (False, False), (False, False), (False, False), (False, True)) + assert d.o2_sensors_alt(m("4100" + "0F")) == ((), (False, False), (False, False), (True, True), (True, True)) + assert d.o2_sensors_alt(m("4100" + "F0")) == ((), (True, True), (True, True), (False, False), (False, False)) + def test_aux_input_status(): - assert d.aux_input_status(m("4100"+"00")) == False - assert d.aux_input_status(m("4100"+"80")) == True + assert d.aux_input_status(m("4100" + "00")) is False + assert d.aux_input_status(m("4100" + "80")) is True + def test_absolute_load(): - assert d.absolute_load(m("4100"+"0000")) == 0 * Unit.percent - assert d.absolute_load(m("4100"+"FFFF")) == 25700 * Unit.percent + assert d.absolute_load(m("4100" + "0000")) == 0 * Unit.percent + assert d.absolute_load(m("4100" + "FFFF")) == 25700 * Unit.percent + def test_elm_voltage(): # these aren't parsed as standard hex messages, so manufacture our own - assert d.elm_voltage([ Message([ Frame("12.875") ]) ]) == 12.875 * Unit.volt - assert d.elm_voltage([ Message([ Frame("12") ]) ]) == 12 * Unit.volt - assert d.elm_voltage([ Message([ Frame(u"12.3V") ]) ]) == 12.3 * Unit.volt - assert d.elm_voltage([ Message([ Frame("12ABCD") ]) ]) == None + assert d.elm_voltage([Message([Frame("12.875")])]) == 12.875 * Unit.volt + assert d.elm_voltage([Message([Frame("12")])]) == 12 * Unit.volt + assert d.elm_voltage([Message([Frame(u"12.3V")])]) == 12.3 * Unit.volt + assert d.elm_voltage([Message([Frame("12ABCD")])]) is None + def test_status(): - status = d.status(m("4100"+"8307FF00")) + status = d.status(m("4100" + "8307FF00")) assert status.MIL assert status.DTC_count == 3 assert status.ignition_type == "spark" @@ -178,18 +210,18 @@ def test_status(): # check that NONE of the compression tests are available for name in COMPRESSION_TESTS: - if name and name not in SPARK_TESTS: # there's one test name in common between spark/compression + if name and name not in SPARK_TESTS: # there's one test name in common between spark/compression assert not status.__dict__[name].available assert not status.__dict__[name].complete - # check that ALL of the spark tests are availablex + # check that ALL of the spark tests are available for name in SPARK_TESTS: if name: assert status.__dict__[name].available assert status.__dict__[name].complete # a different test - status = d.status(m("4100"+"00790303")) + status = d.status(m("4100" + "00790303")) assert not status.MIL assert status.DTC_count == 0 assert status.ignition_type == "compression" @@ -204,7 +236,7 @@ def test_status(): assert not status.FUEL_SYSTEM_MONITORING.complete assert not status.COMPONENT_MONITORING.complete - # check that NONE of the spark tests are availablex + # check that NONE of the spark tests are available for name in SPARK_TESTS: if name and name not in COMPRESSION_TESTS: assert not status.__dict__[name].available @@ -228,45 +260,47 @@ def test_status(): def test_single_dtc(): - assert d.single_dtc(m("4100"+"0104")) == ("P0104", "Mass or Volume Air Flow Circuit Intermittent") - assert d.single_dtc(m("4100"+"4123")) == ("C0123", "") # reverse back into correct bit-order - assert d.single_dtc(m("4100"+"01")) == None - assert d.single_dtc(m("4100"+"010400")) == None + assert d.single_dtc(m("4100" + "0104")) == ("P0104", "Mass or Volume Air Flow Circuit Intermittent") + assert d.single_dtc(m("4100" + "4123")) == ("C0123", "") # reverse back into correct bit-order + assert d.single_dtc(m("4100" + "01")) is None + assert d.single_dtc(m("4100" + "010400")) is None + def test_dtc(): - assert d.dtc(m("4100"+"0104")) == [ + assert d.dtc(m("4100" + "0104")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ] # multiple codes - assert d.dtc(m("4100"+"010480034123")) == [ + assert d.dtc(m("4100" + "010480034123")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), - ("B0003", ""), # unknown error codes return empty strings + ("B0003", ""), # unknown error codes return empty strings ("C0123", ""), ] # invalid code lengths are dropped - assert d.dtc(m("4100"+"0104800341")) == [ + assert d.dtc(m("4100" + "0104800341")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ("B0003", ""), ] # 0000 codes are dropped - assert d.dtc(m("4100"+"000001040000")) == [ + assert d.dtc(m("4100" + "000001040000")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ] # test multiple messages - assert d.dtc(m("4100"+"0104") + m("4100"+"8003") + m("4100"+"0000")) == [ + assert d.dtc(m("4100" + "0104") + m("4100" + "8003") + m("4100" + "0000")) == [ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"), ("B0003", ""), ] + def test_monitor(): # single test ----------------------------------------- # [ test ] - v = d.monitor(m("41"+"01010A0BB00BB00BB0")) - assert len(v) == 1 # 1 test result + v = d.monitor(m("41" + "01010A0BB00BB00BB0")) + assert len(v) == 1 # 1 test result # make sure we can look things up by name and TID assert v[0x01] == v.RTL_THRESHOLD_VOLTAGE == v["RTL_THRESHOLD_VOLTAGE"] @@ -275,13 +309,13 @@ def test_monitor(): assert not v[0x01].is_null() assert float_equals(v[0x01].value, 365 * Unit.millivolt) - assert float_equals(v[0x01].min, 365 * Unit.millivolt) - assert float_equals(v[0x01].max, 365 * Unit.millivolt) + assert float_equals(v[0x01].min, 365 * Unit.millivolt) + assert float_equals(v[0x01].max, 365 * Unit.millivolt) # multiple tests -------------------------------------- # [ test ][ test ][ test ] - v = d.monitor(m("41"+"01010A0BB00BB00BB00105100048000000640185240096004BFFFF")) - assert len(v) == 3 # 3 test results + v = d.monitor(m("41" + "01010A0BB00BB00BB00105100048000000640185240096004BFFFF")) + assert len(v) == 3 # 3 test results # make sure we can look things up by name and TID assert v[0x01] == v.RTL_THRESHOLD_VOLTAGE == v["RTL_THRESHOLD_VOLTAGE"] @@ -293,21 +327,21 @@ def test_monitor(): assert not v[0x85].is_null() assert float_equals(v[0x01].value, 365 * Unit.millivolt) - assert float_equals(v[0x01].min, 365 * Unit.millivolt) - assert float_equals(v[0x01].max, 365 * Unit.millivolt) + assert float_equals(v[0x01].min, 365 * Unit.millivolt) + assert float_equals(v[0x01].max, 365 * Unit.millivolt) assert float_equals(v[0x05].value, 72 * Unit.millisecond) - assert float_equals(v[0x05].min, 0 * Unit.millisecond) - assert float_equals(v[0x05].max, 100 * Unit.millisecond) + assert float_equals(v[0x05].min, 0 * Unit.millisecond) + assert float_equals(v[0x05].max, 100 * Unit.millisecond) assert float_equals(v[0x85].value, 150 * Unit.count) - assert float_equals(v[0x85].min, 75 * Unit.count) - assert float_equals(v[0x85].max, 65535 * Unit.count) + assert float_equals(v[0x85].min, 75 * Unit.count) + assert float_equals(v[0x85].max, 65535 * Unit.count) # truncate incomplete tests ---------------------------- # [ test ][junk] - v = d.monitor(m("41"+"01010A0BB00BB00BB0ABCDEF")) - assert len(v) == 1 # 1 test result + v = d.monitor(m("41" + "01010A0BB00BB00BB0ABCDEF")) + assert len(v) == 1 # 1 test result # make sure we can look things up by name and TID assert v[0x01] == v.RTL_THRESHOLD_VOLTAGE == v["RTL_THRESHOLD_VOLTAGE"] @@ -316,14 +350,13 @@ def test_monitor(): assert not v[0x01].is_null() assert float_equals(v[0x01].value, 365 * Unit.millivolt) - assert float_equals(v[0x01].min, 365 * Unit.millivolt) - assert float_equals(v[0x01].max, 365 * Unit.millivolt) + assert float_equals(v[0x01].min, 365 * Unit.millivolt) + assert float_equals(v[0x01].max, 365 * Unit.millivolt) # truncate incomplete tests ---------------------------- - v = d.monitor(m("41"+"01010A0BB00BB00B")) - assert len(v) == 0 # no valid tests + v = d.monitor(m("41" + "01010A0BB00BB00B")) + assert len(v) == 0 # no valid tests # make sure that the standard tests are null for tid in TEST_IDS: - name = TEST_IDS[tid][0] assert v[tid].is_null() diff --git a/tests/test_obdsim.py b/tests/test_obdsim.py index d3319c45..9367a348 100644 --- a/tests/test_obdsim.py +++ b/tests/test_obdsim.py @@ -1,6 +1,7 @@ - import time + import pytest + from obd import commands, Unit # NOTE: This is purposefully tuned slightly higher than the ELM's default @@ -10,6 +11,7 @@ # ELM's internal timeout. STANDARD_WAIT_TIME = 0.3 + @pytest.fixture(scope="module") def obd(request): """provides an OBD connection object for obdsim""" @@ -32,26 +34,25 @@ def good_rpm_response(r): (r.value >= 0.0 * Unit.rpm) -@pytest.mark.skipif(not pytest.config.getoption("--port"), - reason="needs --port= to run") -def test_supports(obd): - assert(len(obd.supported_commands) > 0) - assert(obd.supports(commands.RPM)) +@pytest.fixture +def skip_if_port_unspecified(request): + if not request.config.getoption("--port"): + pytest.skip("needs --port= to run") + + +def test_supports(skip_if_port_unspecified, obd): + assert (len(obd.supported_commands) > 0) + assert (obd.supports(commands.RPM)) -@pytest.mark.skipif(not pytest.config.getoption("--port"), - reason="needs --port= to run") -def test_rpm(obd): +def test_rpm(skip_if_port_unspecified, obd): r = obd.query(commands.RPM) - assert(good_rpm_response(r)) + assert (good_rpm_response(r)) # Async tests -@pytest.mark.skipif(not pytest.config.getoption("--port"), - reason="needs --port= to run") -def test_async_query(asynchronous): - +def test_async_query(skip_if_port_unspecified, asynchronous): rs = [] asynchronous.watch(commands.RPM) asynchronous.start() @@ -64,14 +65,11 @@ def test_async_query(asynchronous): asynchronous.unwatch_all() # make sure we got data - assert(len(rs) > 0) - assert(all([ good_rpm_response(r) for r in rs ])) - + assert (len(rs) > 0) + assert (all([good_rpm_response(r) for r in rs])) -@pytest.mark.skipif(not pytest.config.getoption("--port"), - reason="needs --port= to run") -def test_async_callback(asynchronous): +def test_async_callback(skip_if_port_unspecified, asynchronous): rs = [] asynchronous.watch(commands.RPM, callback=rs.append) asynchronous.start() @@ -80,32 +78,26 @@ def test_async_callback(asynchronous): asynchronous.unwatch_all() # make sure we got data - assert(len(rs) > 0) - assert(all([ good_rpm_response(r) for r in rs ])) - + assert (len(rs) > 0) + assert (all([good_rpm_response(r) for r in rs])) -@pytest.mark.skipif(not pytest.config.getoption("--port"), - reason="needs --port= to run") -def test_async_paused(asynchronous): - assert(not asynchronous.running) +def test_async_paused(skip_if_port_unspecified, asynchronous): + assert (not asynchronous.running) asynchronous.watch(commands.RPM) asynchronous.start() - assert(asynchronous.running) + assert asynchronous.running with asynchronous.paused() as was_running: - assert(not asynchronous.running) - assert(was_running) + assert not asynchronous.running + assert was_running - assert(asynchronous.running) + assert asynchronous.running asynchronous.stop() - assert(not asynchronous.running) + assert not asynchronous.running -@pytest.mark.skipif(not pytest.config.getoption("--port"), - reason="needs --port= to run") -def test_async_unwatch(asynchronous): - +def test_async_unwatch(skip_if_port_unspecified, asynchronous): watched_rs = [] unwatched_rs = [] @@ -126,18 +118,15 @@ def test_async_unwatch(asynchronous): asynchronous.stop() # the watched commands - assert(len(watched_rs) > 0) - assert(all([ good_rpm_response(r) for r in watched_rs ])) + assert (len(watched_rs) > 0) + assert (all([good_rpm_response(r) for r in watched_rs])) # the unwatched commands - assert(len(unwatched_rs) > 0) - assert(all([ r.is_null() for r in unwatched_rs ])) - + assert (len(unwatched_rs) > 0) + assert (all([r.is_null() for r in unwatched_rs])) -@pytest.mark.skipif(not pytest.config.getoption("--port"), - reason="needs --port= to run") -def test_async_unwatch_callback(asynchronous): +def test_async_unwatch_callback(skip_if_port_unspecified, asynchronous): a_rs = [] b_rs = [] asynchronous.watch(commands.RPM, callback=a_rs.append) @@ -153,5 +142,5 @@ def test_async_unwatch_callback(asynchronous): asynchronous.stop() asynchronous.unwatch_all() - assert(all([ good_rpm_response(r) for r in a_rs + b_rs ])) - assert(len(a_rs) > len(b_rs)) + assert (all([good_rpm_response(r) for r in a_rs + b_rs])) + assert (len(a_rs) > len(b_rs)) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index b623d58b..b2757276 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -1,5 +1,3 @@ - -import random from obd.protocols import * from obd.protocols.protocol import Frame, Message @@ -13,7 +11,7 @@ def test_ECU(): assert (ECU.ALL & ecu) > 0, "ECU: %d is not included in ECU.ALL" % ecu for other_ecu in tested: - assert (ecu & other_ecu) == 0, "ECU: %d has a conflicting bit with another ECU constant" %ecu + assert (ecu & other_ecu) == 0, "ECU: %d has a conflicting bit with another ECU constant" % ecu tested.append(ecu) @@ -22,17 +20,16 @@ def test_frame(): # constructor frame = Frame("asdf") assert frame.raw == "asdf", "Frame failed to accept raw data as __init__ argument" - assert frame.priority == None - assert frame.addr_mode == None - assert frame.rx_id == None - assert frame.tx_id == None - assert frame.type == None - assert frame.seq_index == 0 - assert frame.data_len == None + assert frame.priority is None + assert frame.addr_mode is None + assert frame.rx_id is None + assert frame.tx_id is None + assert frame.type is None + assert frame.seq_index is 0 + assert frame.data_len is None def test_message(): - # constructor frame = Frame("raw input from OBD tool") frame.tx_id = 42 @@ -44,9 +41,9 @@ def test_message(): assert message.frames == frames assert message.ecu == ECU.UNKNOWN - assert message.tx_id == 42 # this is dynamically read from the first frame + assert message.tx_id == 42 # this is dynamically read from the first frame - assert Message([]).tx_id == None # if no frames are given, then we can't report a tx_id + assert Message([]).tx_id is None # if no frames are given, then we can't report a tx_id def test_message_hex(): diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py index 3b0289ff..c0b1b45a 100644 --- a/tests/test_protocol_can.py +++ b/tests/test_protocol_can.py @@ -1,8 +1,6 @@ - import random -from obd.protocols import * -from obd.protocols.protocol import Message +from obd.protocols import * CAN_11_PROTOCOLS = [ ISO_15765_4_11bit_500k, @@ -19,15 +17,13 @@ def check_message(m, num_frames, tx_id, data): """ generic test for correct message values """ assert len(m.frames) == num_frames - assert m.tx_id == tx_id - assert m.data == bytearray(data) - + assert m.tx_id == tx_id + assert m.data == bytearray(data) def test_single_frame(): - for protocol in CAN_11_PROTOCOLS: - p = protocol([]) - + for protocol_ in CAN_11_PROTOCOLS: + p = protocol_([]) r = p(["7E8 06 41 00 00 01 02 03"]) assert len(r) == 1 @@ -65,8 +61,8 @@ def test_hex_straining(): If non-hex values are sent, they should be marked as ECU.UNKNOWN """ - for protocol in CAN_11_PROTOCOLS: - p = protocol([]) + for protocol_ in CAN_11_PROTOCOLS: + p = protocol_([]) # single non-hex message r = p(["12.8 Volts"]) @@ -74,7 +70,6 @@ def test_hex_straining(): assert r[0].ecu == ECU.UNKNOWN assert len(r[0].frames) == 1 - # multiple non-hex message r = p(["12.8 Volts", "NO DATA"]) assert len(r) == 2 @@ -94,14 +89,12 @@ def test_hex_straining(): # second message: invalid, non-parsable non-hex assert r[1].ecu == ECU.UNKNOWN assert len(r[1].frames) == 1 - assert len(r[1].data) == 0 # no data - + assert len(r[1].data) == 0 # no data def test_multi_ecu(): - for protocol in CAN_11_PROTOCOLS: - p = protocol([]) - + for protocol_ in CAN_11_PROTOCOLS: + p = protocol_([]) test_case = [ "7E8 06 41 00 00 01 02 03", @@ -111,7 +104,7 @@ def test_multi_ecu(): correct_data = [0x41, 0x00, 0x00, 0x01, 0x02, 0x03] - # seperate ECUs, single frames each + # separate ECUs, single frames each r = p(test_case) assert len(r) == 3 @@ -121,15 +114,14 @@ def test_multi_ecu(): check_message(r[2], 1, 0x3, correct_data) - def test_multi_line(): """ Tests that valid multiline messages are recombined into single messages. """ - for protocol in CAN_11_PROTOCOLS: - p = protocol([]) + for protocol_ in CAN_11_PROTOCOLS: + p = protocol_([]) test_case = [ "7E8 10 20 49 04 00 01 02 03", @@ -147,21 +139,20 @@ def test_multi_line(): # test a few out-of-order cases for n in range(4): - random.shuffle(test_case) # mix up the frame strings + random.shuffle(test_case) # mix up the frame strings r = p(test_case) assert len(r) == 1 check_message(r[0], len(test_case), 0x0, correct_data) - def test_multi_line_missing_frames(): """ Missing frames in a multi-frame message should drop the message. Tests the contiguity check, and data length byte """ - for protocol in CAN_11_PROTOCOLS: - p = protocol([]) + for protocol_ in CAN_11_PROTOCOLS: + p = protocol_([]) test_case = [ "7E8 10 20 49 04 00 01 02 03", @@ -178,7 +169,6 @@ def test_multi_line_missing_frames(): assert len(r) == 0 - def test_multi_line_mode_03(): """ Tests the special handling of mode 3 commands. @@ -186,8 +176,8 @@ def test_multi_line_mode_03(): in the protocol layer. """ - for protocol in CAN_11_PROTOCOLS: - p = protocol([]) + for protocol_ in CAN_11_PROTOCOLS: + p = protocol_([]) test_case = [ "7E8 10 20 43 04 00 01 02 03", diff --git a/tests/test_protocol_legacy.py b/tests/test_protocol_legacy.py index d7791a1c..8409e92b 100644 --- a/tests/test_protocol_legacy.py +++ b/tests/test_protocol_legacy.py @@ -1,8 +1,6 @@ - import random -from obd.protocols import * -from obd.protocols.protocol import Message +from obd.protocols import * LEGACY_PROTOCOLS = [ SAE_J1850_PWM, @@ -14,15 +12,15 @@ def check_message(m, n_frames, tx_id, data): - """ generic test for correct message values """ - assert len(m.frames) == n_frames - assert m.tx_id == tx_id - assert m.data == bytearray(data) + """ generic test for correct message values """ + assert len(m.frames) == n_frames + assert m.tx_id == tx_id + assert m.data == bytearray(data) def test_single_frame(): - for protocol in LEGACY_PROTOCOLS: - p = protocol([]) + for protocol_ in LEGACY_PROTOCOLS: + p = protocol_([]) # minimum valid length r = p(["48 6B 10 41 00 FF"]) @@ -52,8 +50,8 @@ def test_hex_straining(): If non-hex values are sent, they should be marked as ECU.UNKNOWN """ - for protocol in LEGACY_PROTOCOLS: - p = protocol([]) + for protocol_ in LEGACY_PROTOCOLS: + p = protocol_([]) # single non-hex message r = p(["12.8 Volts"]) @@ -61,7 +59,6 @@ def test_hex_straining(): assert r[0].ecu == ECU.UNKNOWN assert len(r[0].frames) == 1 - # multiple non-hex message r = p(["12.8 Volts", "NO DATA"]) assert len(r) == 2 @@ -81,14 +78,12 @@ def test_hex_straining(): # second message: invalid, non-parsable non-hex assert r[1].ecu == ECU.UNKNOWN assert len(r[1].frames) == 1 - assert len(r[1].data) == 0 # no data - + assert len(r[1].data) == 0 # no data def test_multi_ecu(): - for protocol in LEGACY_PROTOCOLS: - p = protocol([]) - + for protocol_ in LEGACY_PROTOCOLS: + p = protocol_([]) test_case = [ "48 6B 13 41 00 00 01 02 03 FF", @@ -98,7 +93,7 @@ def test_multi_ecu(): correct_data = [0x41, 0x00, 0x00, 0x01, 0x02, 0x03] - # seperate ECUs, single frames each + # separate ECUs, single frames each r = p(test_case) assert len(r) == len(test_case) @@ -108,15 +103,14 @@ def test_multi_ecu(): check_message(r[2], 1, 0x13, correct_data) - def test_multi_line(): """ Tests that valid multiline messages are recombined into single messages. """ - for protocol in LEGACY_PROTOCOLS: - p = protocol([]) + for protocol_ in LEGACY_PROTOCOLS: + p = protocol_([]) test_case = [ "48 6B 10 49 02 01 00 01 02 03 FF", @@ -133,22 +127,20 @@ def test_multi_line(): # test a few out-of-order cases for n in range(4): - random.shuffle(test_case) # mix up the frame strings + random.shuffle(test_case) # mix up the frame strings r = p(test_case) assert len(r) == 1 check_message(r[0], len(test_case), 0x10, correct_data) - def test_multi_line_missing_frames(): """ Missing frames in a multi-frame message should drop the message. Tests the contiguity check, and data length byte """ - for protocol in LEGACY_PROTOCOLS: - p = protocol([]) - + for protocol_ in LEGACY_PROTOCOLS: + p = protocol_([]) test_case = [ "48 6B 10 49 02 01 00 01 02 03 FF", @@ -170,16 +162,15 @@ def test_multi_line_mode_03(): An extra byte is fudged in to make the output look like CAN """ - for protocol in LEGACY_PROTOCOLS: - p = protocol([]) - + for protocol_ in LEGACY_PROTOCOLS: + p = protocol_([]) test_case = [ "48 6B 10 43 00 01 02 03 04 05 FF", "48 6B 10 43 06 07 08 09 0A 0B FF", ] - correct_data = [0x43, 0x00] + list(range(12)) # data is stitched in order recieved + correct_data = [0x43, 0x00] + list(range(12)) # data is stitched in order received # ^^^^ this is an arbitrary value in the source code r = p(test_case) diff --git a/tests/test_uas.py b/tests/test_uas.py index 142f9426..a9508c1d 100644 --- a/tests/test_uas.py +++ b/tests/test_uas.py @@ -1,14 +1,15 @@ - from binascii import unhexlify from obd.UnitsAndScaling import Unit, UAS_IDS -# shim to convert human-readable hex into bytearray +# shim to convert human-readable hex into byte-array def b(_hex): return bytearray(unhexlify(_hex)) + FLOAT_EQUALS_TOLERANCE = 0.025 + # comparison for pint floating point values def float_equals(va, vb): units_match = (va.u == vb.u) @@ -20,124 +21,146 @@ def float_equals(va, vb): Unsigned Units """ + def test_01(): assert UAS_IDS[0x01](b("0000")) == 0 * Unit.count assert UAS_IDS[0x01](b("0001")) == 1 * Unit.count assert UAS_IDS[0x01](b("FFFF")) == 65535 * Unit.count + def test_02(): assert UAS_IDS[0x02](b("0000")) == 0 * Unit.count assert UAS_IDS[0x02](b("0001")) == 0.1 * Unit.count assert UAS_IDS[0x02](b("FFFF")) == 6553.5 * Unit.count + def test_03(): assert UAS_IDS[0x03](b("0000")) == 0 * Unit.count assert UAS_IDS[0x03](b("0001")) == 0.01 * Unit.count assert UAS_IDS[0x03](b("FFFF")) == 655.35 * Unit.count + def test_04(): assert UAS_IDS[0x04](b("0000")) == 0 * Unit.count assert UAS_IDS[0x04](b("0001")) == 0.001 * Unit.count assert UAS_IDS[0x04](b("FFFF")) == 65.535 * Unit.count + def test_05(): assert float_equals(UAS_IDS[0x05](b("0000")), 0 * Unit.count) assert float_equals(UAS_IDS[0x05](b("0001")), 0.0000305 * Unit.count) assert float_equals(UAS_IDS[0x05](b("FFFF")), 1.9999 * Unit.count) + def test_06(): assert float_equals(UAS_IDS[0x06](b("0000")), 0 * Unit.count) assert float_equals(UAS_IDS[0x06](b("0001")), 0.000305 * Unit.count) assert float_equals(UAS_IDS[0x06](b("FFFF")), 19.988 * Unit.count) + def test_07(): assert float_equals(UAS_IDS[0x07](b("0000")), 0 * Unit.rpm) assert float_equals(UAS_IDS[0x07](b("0002")), 0.5 * Unit.rpm) assert float_equals(UAS_IDS[0x07](b("FFFD")), 16383.25 * Unit.rpm) assert float_equals(UAS_IDS[0x07](b("FFFF")), 16383.75 * Unit.rpm) + def test_08(): assert float_equals(UAS_IDS[0x08](b("0000")), 0 * Unit.kph) assert float_equals(UAS_IDS[0x08](b("0064")), 1 * Unit.kph) assert float_equals(UAS_IDS[0x08](b("03E7")), 9.99 * Unit.kph) assert float_equals(UAS_IDS[0x08](b("FFFF")), 655.35 * Unit.kph) + def test_09(): assert float_equals(UAS_IDS[0x09](b("0000")), 0 * Unit.kph) assert float_equals(UAS_IDS[0x09](b("0064")), 100 * Unit.kph) assert float_equals(UAS_IDS[0x09](b("03E7")), 999 * Unit.kph) assert float_equals(UAS_IDS[0x09](b("FFFF")), 65535 * Unit.kph) + def test_0A(): # the standard gives example values that don't line up perfectly # with the scale. The last two tests here deviate from the standard assert float_equals(UAS_IDS[0x0A](b("0000")), 0 * Unit.millivolt) assert float_equals(UAS_IDS[0x0A](b("0001")), 0.122 * Unit.millivolt) - assert float_equals(UAS_IDS[0x0A](b("2004")), 999.912 * Unit.millivolt) # 1000.488 mV - assert float_equals(UAS_IDS[0x0A](b("FFFF")), 7995.27 * Unit.millivolt) # 7999 mV + assert float_equals(UAS_IDS[0x0A](b("2004")), 999.912 * Unit.millivolt) # 1000.488 mV + assert float_equals(UAS_IDS[0x0A](b("FFFF")), 7995.27 * Unit.millivolt) # 7999 mV + def test_0B(): assert UAS_IDS[0x0B](b("0000")) == 0 * Unit.volt assert UAS_IDS[0x0B](b("0001")) == 0.001 * Unit.volt assert UAS_IDS[0x0B](b("FFFF")) == 65.535 * Unit.volt + def test_0C(): assert float_equals(UAS_IDS[0x0C](b("0000")), 0 * Unit.volt) assert float_equals(UAS_IDS[0x0C](b("0001")), 0.01 * Unit.volt) assert float_equals(UAS_IDS[0x0C](b("FFFF")), 655.350 * Unit.volt) + def test_0D(): assert float_equals(UAS_IDS[0x0D](b("0000")), 0 * Unit.milliampere) assert float_equals(UAS_IDS[0x0D](b("0001")), 0.004 * Unit.milliampere) assert float_equals(UAS_IDS[0x0D](b("8000")), 128 * Unit.milliampere) assert float_equals(UAS_IDS[0x0D](b("FFFF")), 255.996 * Unit.milliampere) + def test_0E(): assert UAS_IDS[0x0E](b("0000")) == 0 * Unit.ampere assert UAS_IDS[0x0E](b("8000")) == 32.768 * Unit.ampere assert UAS_IDS[0x0E](b("FFFF")) == 65.535 * Unit.ampere + def test_0F(): assert UAS_IDS[0x0F](b("0000")) == 0 * Unit.ampere assert UAS_IDS[0x0F](b("0001")) == 0.01 * Unit.ampere assert UAS_IDS[0x0F](b("FFFF")) == 655.35 * Unit.ampere + def test_10(): assert UAS_IDS[0x10](b("0000")) == 0 * Unit.millisecond assert UAS_IDS[0x10](b("8000")) == 32768 * Unit.millisecond assert UAS_IDS[0x10](b("EA60")) == 60000 * Unit.millisecond assert UAS_IDS[0x10](b("FFFF")) == 65535 * Unit.millisecond + def test_11(): assert UAS_IDS[0x11](b("0000")) == 0 * Unit.millisecond assert UAS_IDS[0x11](b("8000")) == 3276800 * Unit.millisecond assert UAS_IDS[0x11](b("EA60")) == 6000000 * Unit.millisecond assert UAS_IDS[0x11](b("FFFF")) == 6553500 * Unit.millisecond + def test_12(): assert UAS_IDS[0x12](b("0000")) == 0 * Unit.second assert UAS_IDS[0x12](b("003C")) == 60 * Unit.second assert UAS_IDS[0x12](b("0E10")) == 3600 * Unit.second assert UAS_IDS[0x12](b("FFFF")) == 65535 * Unit.second + def test_13(): assert UAS_IDS[0x13](b("0000")) == 0 * Unit.milliohm assert UAS_IDS[0x13](b("0001")) == 1 * Unit.milliohm assert UAS_IDS[0x13](b("8000")) == 32768 * Unit.milliohm assert UAS_IDS[0x13](b("FFFF")) == 65535 * Unit.milliohm + def test_14(): assert UAS_IDS[0x14](b("0000")) == 0 * Unit.ohm assert UAS_IDS[0x14](b("0001")) == 1 * Unit.ohm assert UAS_IDS[0x14](b("8000")) == 32768 * Unit.ohm assert UAS_IDS[0x14](b("FFFF")) == 65535 * Unit.ohm + def test_15(): assert UAS_IDS[0x15](b("0000")) == 0 * Unit.kiloohm assert UAS_IDS[0x15](b("0001")) == 1 * Unit.kiloohm assert UAS_IDS[0x15](b("8000")) == 32768 * Unit.kiloohm assert UAS_IDS[0x15](b("FFFF")) == 65535 * Unit.kiloohm + def test_16(): assert UAS_IDS[0x16](b("0000")) == Unit.Quantity(-40, Unit.celsius) assert UAS_IDS[0x16](b("0001")) == Unit.Quantity(-39.9, Unit.celsius) @@ -145,47 +168,56 @@ def test_16(): assert UAS_IDS[0x16](b("0190")) == Unit.Quantity(0, Unit.celsius) assert UAS_IDS[0x16](b("FFFF")) == Unit.Quantity(6513.5, Unit.celsius) + def test_17(): assert UAS_IDS[0x17](b("0000")) == 0 * Unit.kilopascal assert UAS_IDS[0x17](b("0001")) == 0.01 * Unit.kilopascal assert UAS_IDS[0x17](b("FFFF")) == 655.35 * Unit.kilopascal + def test_18(): assert UAS_IDS[0x18](b("0000")) == 0 * Unit.kilopascal assert UAS_IDS[0x18](b("0001")) == 0.0117 * Unit.kilopascal assert UAS_IDS[0x18](b("FFFF")) == 766.7595 * Unit.kilopascal + def test_19(): assert UAS_IDS[0x19](b("0000")) == 0 * Unit.kilopascal assert UAS_IDS[0x19](b("0001")) == 0.079 * Unit.kilopascal assert UAS_IDS[0x19](b("FFFF")) == 5177.265 * Unit.kilopascal + def test_1A(): assert UAS_IDS[0x1A](b("0000")) == 0 * Unit.kilopascal assert UAS_IDS[0x1A](b("0001")) == 1 * Unit.kilopascal assert UAS_IDS[0x1A](b("FFFF")) == 65535 * Unit.kilopascal + def test_1B(): assert UAS_IDS[0x1B](b("0000")) == 0 * Unit.kilopascal assert UAS_IDS[0x1B](b("0001")) == 10 * Unit.kilopascal assert UAS_IDS[0x1B](b("FFFF")) == 655350 * Unit.kilopascal + def test_1C(): assert UAS_IDS[0x1C](b("0000")) == 0 * Unit.degree assert UAS_IDS[0x1C](b("0001")) == 0.01 * Unit.degree assert UAS_IDS[0x1C](b("8CA0")) == 360 * Unit.degree assert UAS_IDS[0x1C](b("FFFF")) == 655.35 * Unit.degree + def test_1D(): assert UAS_IDS[0x1D](b("0000")) == 0 * Unit.degree assert UAS_IDS[0x1D](b("0001")) == 0.5 * Unit.degree assert UAS_IDS[0x1D](b("FFFF")) == 32767.5 * Unit.degree + def test_1E(): assert float_equals(UAS_IDS[0x1E](b("0000")), 0 * Unit.ratio) assert float_equals(UAS_IDS[0x1E](b("8013")), 1 * Unit.ratio) assert float_equals(UAS_IDS[0x1E](b("FFFF")), 1.999 * Unit.ratio) + def test_1F(): assert float_equals(UAS_IDS[0x1F](b("0000")), 0 * Unit.ratio) assert float_equals(UAS_IDS[0x1F](b("0001")), 0.05 * Unit.ratio) @@ -193,80 +225,96 @@ def test_1F(): assert float_equals(UAS_IDS[0x1F](b("0126")), 14.7 * Unit.ratio) assert float_equals(UAS_IDS[0x1F](b("FFFF")), 3276.75 * Unit.ratio) + def test_20(): assert float_equals(UAS_IDS[0x20](b("0000")), 0 * Unit.ratio) assert float_equals(UAS_IDS[0x20](b("0001")), 0.0039062 * Unit.ratio) assert float_equals(UAS_IDS[0x20](b("FFFF")), 255.993 * Unit.ratio) + def test_21(): assert UAS_IDS[0x21](b("0000")) == 0 * Unit.millihertz assert UAS_IDS[0x21](b("8000")) == 32768 * Unit.millihertz assert UAS_IDS[0x21](b("FFFF")) == 65535 * Unit.millihertz + def test_22(): assert UAS_IDS[0x22](b("0000")) == 0 * Unit.hertz assert UAS_IDS[0x22](b("8000")) == 32768 * Unit.hertz assert UAS_IDS[0x22](b("FFFF")) == 65535 * Unit.hertz + def test_23(): assert UAS_IDS[0x23](b("0000")) == 0 * Unit.kilohertz assert UAS_IDS[0x23](b("8000")) == 32768 * Unit.kilohertz assert UAS_IDS[0x23](b("FFFF")) == 65535 * Unit.kilohertz + def test_24(): assert UAS_IDS[0x24](b("0000")) == 0 * Unit.count assert UAS_IDS[0x24](b("0001")) == 1 * Unit.count assert UAS_IDS[0x24](b("FFFF")) == 65535 * Unit.count + def test_25(): assert UAS_IDS[0x25](b("0000")) == 0 * Unit.kilometer assert UAS_IDS[0x25](b("0001")) == 1 * Unit.kilometer assert UAS_IDS[0x25](b("FFFF")) == 65535 * Unit.kilometer + def test_26(): assert UAS_IDS[0x26](b("0000")) == 0 * Unit.millivolt / Unit.millisecond assert UAS_IDS[0x26](b("0001")) == 0.1 * Unit.millivolt / Unit.millisecond assert UAS_IDS[0x26](b("FFFF")) == 6553.5 * Unit.millivolt / Unit.millisecond + def test_27(): assert UAS_IDS[0x27](b("0000")) == 0 * Unit.grams_per_second assert UAS_IDS[0x27](b("0001")) == 0.01 * Unit.grams_per_second assert UAS_IDS[0x27](b("FFFF")) == 655.35 * Unit.grams_per_second + def test_28(): assert UAS_IDS[0x28](b("0000")) == 0 * Unit.grams_per_second assert UAS_IDS[0x28](b("0001")) == 1 * Unit.grams_per_second assert UAS_IDS[0x28](b("FFFF")) == 65535 * Unit.grams_per_second + def test_29(): assert UAS_IDS[0x29](b("0000")) == 0 * Unit.pascal / Unit.second assert UAS_IDS[0x29](b("0004")) == 1 * Unit.pascal / Unit.second - assert UAS_IDS[0x29](b("FFFF")) == 16383.75 * Unit.pascal / Unit.second # deviates from standard examples + assert UAS_IDS[0x29](b("FFFF")) == 16383.75 * Unit.pascal / Unit.second # deviates from standard examples + def test_2A(): assert UAS_IDS[0x2A](b("0000")) == 0 * Unit.kilogram / Unit.hour assert UAS_IDS[0x2A](b("0001")) == 0.001 * Unit.kilogram / Unit.hour assert UAS_IDS[0x2A](b("FFFF")) == 65.535 * Unit.kilogram / Unit.hour + def test_2B(): assert UAS_IDS[0x2B](b("0000")) == 0 * Unit.count assert UAS_IDS[0x2B](b("0001")) == 1 * Unit.count assert UAS_IDS[0x2B](b("FFFF")) == 65535 * Unit.count + def test_2C(): assert UAS_IDS[0x2C](b("0000")) == 0 * Unit.gram assert UAS_IDS[0x2C](b("0001")) == 0.01 * Unit.gram assert UAS_IDS[0x2C](b("FFFF")) == 655.35 * Unit.gram + def test_2D(): assert UAS_IDS[0x2D](b("0000")) == 0 * Unit.milligram assert UAS_IDS[0x2D](b("0001")) == 0.01 * Unit.milligram assert UAS_IDS[0x2D](b("FFFF")) == 655.35 * Unit.milligram + def test_2E(): - assert UAS_IDS[0x2E](b("0000")) == False - assert UAS_IDS[0x2E](b("0001")) == True - assert UAS_IDS[0x2E](b("FFFF")) == True + assert UAS_IDS[0x2E](b("0000")) is False + assert UAS_IDS[0x2E](b("0001")) is True + assert UAS_IDS[0x2E](b("FFFF")) is True + def test_2F(): assert UAS_IDS[0x2F](b("0000")) == 0 * Unit.percent @@ -274,22 +322,26 @@ def test_2F(): assert UAS_IDS[0x2F](b("2710")) == 100 * Unit.percent assert UAS_IDS[0x2F](b("FFFF")) == 655.35 * Unit.percent + def test_30(): assert float_equals(UAS_IDS[0x30](b("0000")), 0 * Unit.percent) assert float_equals(UAS_IDS[0x30](b("0001")), 0.001526 * Unit.percent) assert float_equals(UAS_IDS[0x30](b("FFFF")), 100.00641 * Unit.percent) + def test_31(): assert UAS_IDS[0x31](b("0000")) == 0 * Unit.liter assert UAS_IDS[0x31](b("0001")) == 0.001 * Unit.liter assert UAS_IDS[0x31](b("FFFF")) == 65.535 * Unit.liter + def test_32(): assert float_equals(UAS_IDS[0x32](b("0000")), 0 * Unit.inch) assert float_equals(UAS_IDS[0x32](b("0010")), 0.0004883 * Unit.inch) assert float_equals(UAS_IDS[0x32](b("0011")), 0.0005188 * Unit.inch) assert float_equals(UAS_IDS[0x32](b("FFFF")), 1.9999695 * Unit.inch) + def test_33(): assert float_equals(UAS_IDS[0x33](b("0000")), 0 * Unit.ratio) assert float_equals(UAS_IDS[0x33](b("0001")), 0.00024414 * Unit.ratio) @@ -297,33 +349,39 @@ def test_33(): assert float_equals(UAS_IDS[0x33](b("E5BE")), 14.36 * Unit.ratio) assert float_equals(UAS_IDS[0x33](b("FFFF")), 16.0 * Unit.ratio) + def test_34(): assert UAS_IDS[0x34](b("0000")) == 0 * Unit.minute assert UAS_IDS[0x34](b("003C")) == 60 * Unit.minute assert UAS_IDS[0x34](b("0E10")) == 3600 * Unit.minute assert UAS_IDS[0x34](b("FFFF")) == 65535 * Unit.minute + def test_35(): assert UAS_IDS[0x35](b("0000")) == 0 * Unit.millisecond assert UAS_IDS[0x35](b("8000")) == 327680 * Unit.millisecond assert UAS_IDS[0x35](b("EA60")) == 600000 * Unit.millisecond assert UAS_IDS[0x35](b("FFFF")) == 655350 * Unit.millisecond + def test_36(): assert UAS_IDS[0x36](b("0000")) == 0 * Unit.gram assert UAS_IDS[0x36](b("0001")) == 0.01 * Unit.gram assert UAS_IDS[0x36](b("FFFF")) == 655.35 * Unit.gram + def test_37(): assert UAS_IDS[0x37](b("0000")) == 0 * Unit.gram assert UAS_IDS[0x37](b("0001")) == 0.1 * Unit.gram assert UAS_IDS[0x37](b("FFFF")) == 6553.5 * Unit.gram + def test_38(): assert UAS_IDS[0x38](b("0000")) == 0 * Unit.gram assert UAS_IDS[0x38](b("0001")) == 1 * Unit.gram assert UAS_IDS[0x38](b("FFFF")) == 65535 * Unit.gram + def test_39(): assert float_equals(UAS_IDS[0x39](b("0000")), -327.68 * Unit.percent) assert float_equals(UAS_IDS[0x39](b("58F0")), -100 * Unit.percent) @@ -333,54 +391,61 @@ def test_39(): assert float_equals(UAS_IDS[0x39](b("A710")), 100 * Unit.percent) assert float_equals(UAS_IDS[0x39](b("FFFF")), 327.67 * Unit.percent) + def test_3A(): assert UAS_IDS[0x3A](b("0000")) == 0 * Unit.gram assert UAS_IDS[0x3A](b("0001")) == 0.001 * Unit.gram assert UAS_IDS[0x3A](b("FFFF")) == 65.535 * Unit.gram + def test_3B(): assert float_equals(UAS_IDS[0x3B](b("0000")), 0 * Unit.gram) assert float_equals(UAS_IDS[0x3B](b("0001")), 0.0001 * Unit.gram) assert float_equals(UAS_IDS[0x3B](b("FFFF")), 6.5535 * Unit.gram) + def test_3C(): assert UAS_IDS[0x3C](b("0000")) == 0 * Unit.microsecond assert UAS_IDS[0x3C](b("8000")) == 3276.8 * Unit.microsecond assert UAS_IDS[0x3C](b("EA60")) == 6000.0 * Unit.microsecond assert UAS_IDS[0x3C](b("FFFF")) == 6553.5 * Unit.microsecond + def test_3D(): assert UAS_IDS[0x3D](b("0000")) == 0 * Unit.milliampere assert UAS_IDS[0x3D](b("0001")) == 0.01 * Unit.milliampere assert UAS_IDS[0x3D](b("FFFF")) == 655.35 * Unit.milliampere + def test_3E(): assert float_equals(UAS_IDS[0x3E](b("0000")), 0 * Unit.millimeter ** 2) assert float_equals(UAS_IDS[0x3E](b("8000")), 1.9999 * Unit.millimeter ** 2) assert float_equals(UAS_IDS[0x3E](b("FFFF")), 3.9999 * Unit.millimeter ** 2) + def test_3F(): assert UAS_IDS[0x3F](b("0000")) == 0 * Unit.liter assert UAS_IDS[0x3F](b("0001")) == 0.01 * Unit.liter assert UAS_IDS[0x3F](b("FFFF")) == 655.35 * Unit.liter + def test_40(): assert UAS_IDS[0x40](b("0000")) == 0 * Unit.ppm assert UAS_IDS[0x40](b("0001")) == 1 * Unit.ppm assert UAS_IDS[0x40](b("FFFF")) == 65535 * Unit.ppm + def test_41(): assert UAS_IDS[0x41](b("0000")) == 0 * Unit.microampere assert UAS_IDS[0x41](b("0001")) == 0.01 * Unit.microampere assert UAS_IDS[0x41](b("FFFF")) == 655.35 * Unit.microampere - - """ signed Units """ + def test_81(): assert UAS_IDS[0x81](b("8000")) == -32768 * Unit.count assert UAS_IDS[0x81](b("FFFF")) == -1 * Unit.count @@ -388,6 +453,7 @@ def test_81(): assert UAS_IDS[0x81](b("0001")) == 1 * Unit.count assert UAS_IDS[0x81](b("7FFF")) == 32767 * Unit.count + def test_82(): assert UAS_IDS[0x82](b("8000")) == -3276.8 * Unit.count assert UAS_IDS[0x82](b("FFFF")) == -0.1 * Unit.count @@ -395,6 +461,7 @@ def test_82(): assert UAS_IDS[0x82](b("0001")) == 0.1 * Unit.count assert float_equals(UAS_IDS[0x82](b("7FFF")), 3276.7 * Unit.count) + def test_83(): assert UAS_IDS[0x83](b("8000")) == -327.68 * Unit.count assert UAS_IDS[0x83](b("FFFF")) == -0.01 * Unit.count @@ -402,6 +469,7 @@ def test_83(): assert UAS_IDS[0x83](b("0001")) == 0.01 * Unit.count assert float_equals(UAS_IDS[0x83](b("7FFF")), 327.67 * Unit.count) + def test_84(): assert UAS_IDS[0x84](b("8000")) == -32.768 * Unit.count assert UAS_IDS[0x84](b("FFFF")) == -0.001 * Unit.count @@ -409,6 +477,7 @@ def test_84(): assert UAS_IDS[0x84](b("0001")) == 0.001 * Unit.count assert float_equals(UAS_IDS[0x84](b("7FFF")), 32.767 * Unit.count) + def test_85(): assert float_equals(UAS_IDS[0x85](b("8000")), -0.9999995 * Unit.count) assert float_equals(UAS_IDS[0x85](b("FFFF")), -0.0000305 * Unit.count) @@ -416,6 +485,7 @@ def test_85(): assert float_equals(UAS_IDS[0x85](b("0001")), 0.0000305 * Unit.count) assert float_equals(UAS_IDS[0x85](b("7FFF")), 0.9999995 * Unit.count) + def test_86(): assert float_equals(UAS_IDS[0x86](b("8000")), -9.999995 * Unit.count) assert float_equals(UAS_IDS[0x86](b("FFFF")), -0.000305 * Unit.count) @@ -423,6 +493,7 @@ def test_86(): assert float_equals(UAS_IDS[0x86](b("0001")), 0.000305 * Unit.count) assert float_equals(UAS_IDS[0x86](b("7FFF")), 9.999995 * Unit.count) + def test_87(): assert UAS_IDS[0x87](b("8000")) == -32768 * Unit.ppm assert UAS_IDS[0x87](b("FFFF")) == -1 * Unit.ppm @@ -430,14 +501,16 @@ def test_87(): assert UAS_IDS[0x87](b("0001")) == 1 * Unit.ppm assert UAS_IDS[0x87](b("7FFF")) == 32767 * Unit.ppm + def test_8A(): # the standard gives example values that don't line up perfectly # with the scale. The last two tests here deviate from the standard - assert float_equals(UAS_IDS[0x8A](b("8000")), -3997.696 * Unit.millivolt) # -3999.998 mV + assert float_equals(UAS_IDS[0x8A](b("8000")), -3997.696 * Unit.millivolt) # -3999.998 mV assert float_equals(UAS_IDS[0x8A](b("FFFF")), -0.122 * Unit.millivolt) assert float_equals(UAS_IDS[0x8A](b("0000")), 0 * Unit.millivolt) assert float_equals(UAS_IDS[0x8A](b("0001")), 0.122 * Unit.millivolt) - assert float_equals(UAS_IDS[0x8A](b("7FFF")), 3997.574 * Unit.millivolt) # 3999.876 mV + assert float_equals(UAS_IDS[0x8A](b("7FFF")), 3997.574 * Unit.millivolt) # 3999.876 mV + def test_8B(): assert UAS_IDS[0x8B](b("8000")) == -32.768 * Unit.volt @@ -446,6 +519,7 @@ def test_8B(): assert UAS_IDS[0x8B](b("0001")) == 0.001 * Unit.volt assert UAS_IDS[0x8B](b("7FFF")) == 32.767 * Unit.volt + def test_8C(): assert UAS_IDS[0x8C](b("8000")) == -327.68 * Unit.volt assert UAS_IDS[0x8C](b("FFFF")) == -0.01 * Unit.volt @@ -453,6 +527,7 @@ def test_8C(): assert UAS_IDS[0x8C](b("0001")) == 0.01 * Unit.volt assert UAS_IDS[0x8C](b("7FFF")) == 327.67 * Unit.volt + def test_8D(): assert float_equals(UAS_IDS[0x8D](b("8000")), -128 * Unit.milliampere) assert float_equals(UAS_IDS[0x8D](b("FFFF")), -0.00390625 * Unit.milliampere) @@ -460,6 +535,7 @@ def test_8D(): assert float_equals(UAS_IDS[0x8D](b("0001")), 0.00390625 * Unit.milliampere) assert float_equals(UAS_IDS[0x8D](b("7FFF")), 127.996 * Unit.milliampere) + def test_8E(): assert UAS_IDS[0x8E](b("8000")) == -32.768 * Unit.ampere assert UAS_IDS[0x8E](b("FFFF")) == -0.001 * Unit.ampere @@ -467,6 +543,7 @@ def test_8E(): assert UAS_IDS[0x8E](b("0001")) == 0.001 * Unit.ampere assert UAS_IDS[0x8E](b("7FFF")) == 32.767 * Unit.ampere + def test_90(): assert UAS_IDS[0x90](b("8000")) == -32768 * Unit.millisecond assert UAS_IDS[0x90](b("FFFF")) == -1 * Unit.millisecond @@ -474,6 +551,7 @@ def test_90(): assert UAS_IDS[0x90](b("0001")) == 1 * Unit.millisecond assert UAS_IDS[0x90](b("7FFF")) == 32767 * Unit.millisecond + def test_96(): assert float_equals(UAS_IDS[0x96](b("8000")), Unit.Quantity(-3276.8, Unit.celsius)) assert float_equals(UAS_IDS[0x96](b("FFFF")), Unit.Quantity(-0.1, Unit.celsius)) @@ -481,6 +559,7 @@ def test_96(): assert float_equals(UAS_IDS[0x96](b("0001")), Unit.Quantity(0.1, Unit.celsius)) assert float_equals(UAS_IDS[0x96](b("7FFF")), Unit.Quantity(3276.7, Unit.celsius)) + def test_99(): assert float_equals(UAS_IDS[0x99](b("8000")), -3276.8 * Unit.kilopascal) assert float_equals(UAS_IDS[0x99](b("FFFF")), -0.1 * Unit.kilopascal) @@ -488,6 +567,7 @@ def test_99(): assert float_equals(UAS_IDS[0x99](b("0001")), 0.1 * Unit.kilopascal) assert float_equals(UAS_IDS[0x99](b("7FFF")), 3276.7 * Unit.kilopascal) + def test_9C(): assert UAS_IDS[0x9C](b("8000")) == -327.68 * Unit.degree assert UAS_IDS[0x9C](b("FFFF")) == -0.01 * Unit.degree @@ -495,6 +575,7 @@ def test_9C(): assert UAS_IDS[0x9C](b("0001")) == 0.01 * Unit.degree assert UAS_IDS[0x9C](b("7FFF")) == 327.67 * Unit.degree + def test_9D(): assert UAS_IDS[0x9D](b("8000")) == -16384 * Unit.degree assert UAS_IDS[0x9D](b("FFFF")) == -0.5 * Unit.degree @@ -502,6 +583,7 @@ def test_9D(): assert UAS_IDS[0x9D](b("0001")) == 0.5 * Unit.degree assert UAS_IDS[0x9D](b("7FFF")) == 16383.5 * Unit.degree + def test_A8(): assert UAS_IDS[0xA8](b("8000")) == -32768 * Unit.grams_per_second assert UAS_IDS[0xA8](b("FFFF")) == -1 * Unit.grams_per_second @@ -509,6 +591,7 @@ def test_A8(): assert UAS_IDS[0xA8](b("0001")) == 1 * Unit.grams_per_second assert UAS_IDS[0xA8](b("7FFF")) == 32767 * Unit.grams_per_second + def test_A9(): assert UAS_IDS[0xA9](b("8000")) == -8192 * Unit.pascal / Unit.second assert UAS_IDS[0xA9](b("FFFC")) == -1 * Unit.pascal / Unit.second @@ -516,6 +599,7 @@ def test_A9(): assert UAS_IDS[0xA9](b("0004")) == 1 * Unit.pascal / Unit.second assert UAS_IDS[0xA9](b("7FFF")) == 8191.75 * Unit.pascal / Unit.second + def test_AD(): assert UAS_IDS[0xAD](b("8000")) == -327.68 * Unit.milligram assert UAS_IDS[0xAD](b("FFFF")) == -0.01 * Unit.milligram @@ -523,6 +607,7 @@ def test_AD(): assert UAS_IDS[0xAD](b("0001")) == 0.01 * Unit.milligram assert UAS_IDS[0xAD](b("7FFF")) == 327.67 * Unit.milligram + def test_AE(): assert UAS_IDS[0xAE](b("8000")) == -3276.8 * Unit.milligram assert UAS_IDS[0xAE](b("FFFF")) == -0.1 * Unit.milligram @@ -530,6 +615,7 @@ def test_AE(): assert UAS_IDS[0xAE](b("0001")) == 0.1 * Unit.milligram assert float_equals(UAS_IDS[0xAE](b("7FFF")), 3276.7 * Unit.milligram) + def test_AF(): assert UAS_IDS[0xAF](b("8000")) == -327.68 * Unit.percent assert UAS_IDS[0xAF](b("FFFF")) == -0.01 * Unit.percent @@ -537,6 +623,7 @@ def test_AF(): assert UAS_IDS[0xAF](b("0001")) == 0.01 * Unit.percent assert UAS_IDS[0xAF](b("7FFF")) == 327.67 * Unit.percent + def test_B0(): assert UAS_IDS[0xB0](b("8000")) == -100.007936 * Unit.percent assert UAS_IDS[0xB0](b("FFFF")) == -0.003052 * Unit.percent @@ -544,6 +631,7 @@ def test_B0(): assert UAS_IDS[0xB0](b("0001")) == 0.003052 * Unit.percent assert UAS_IDS[0xB0](b("7FFF")) == 100.004884 * Unit.percent + def test_B1(): assert UAS_IDS[0xB1](b("8000")) == -65536 * Unit.millivolt / Unit.second assert UAS_IDS[0xB1](b("FFFF")) == -2 * Unit.millivolt / Unit.second @@ -551,6 +639,7 @@ def test_B1(): assert UAS_IDS[0xB1](b("0001")) == 2 * Unit.millivolt / Unit.second assert UAS_IDS[0xB1](b("7FFF")) == 65534 * Unit.millivolt / Unit.second + def test_FC(): assert UAS_IDS[0xFC](b("8000")) == -327.68 * Unit.kilopascal assert UAS_IDS[0xFC](b("FFFF")) == -0.01 * Unit.kilopascal @@ -558,6 +647,7 @@ def test_FC(): assert UAS_IDS[0xFC](b("0001")) == 0.01 * Unit.kilopascal assert UAS_IDS[0xFC](b("7FFF")) == 327.67 * Unit.kilopascal + def test_FD(): assert UAS_IDS[0xFD](b("8000")) == -32.768 * Unit.kilopascal assert UAS_IDS[0xFD](b("FFFF")) == -0.001 * Unit.kilopascal @@ -565,6 +655,7 @@ def test_FD(): assert UAS_IDS[0xFD](b("0001")) == 0.001 * Unit.kilopascal assert UAS_IDS[0xFD](b("7FFF")) == 32.767 * Unit.kilopascal + def test_FE(): assert UAS_IDS[0xFE](b("8000")) == -8192 * Unit.pascal assert UAS_IDS[0xFE](b("FFFC")) == -1 * Unit.pascal From 36fe50d932c874dd86864cb716fdae2e43136278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kaczmarczyk?= Date: Wed, 27 Feb 2019 22:23:59 -0800 Subject: [PATCH 525/569] travis: Add tox test coverage Signed-off-by: Alistair Francis --- .gitignore | 3 ++- .travis.yml | 34 +++++++++++++++++++++-------- tox.ini | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index f2abd626..b85b092d 100644 --- a/.gitignore +++ b/.gitignore @@ -36,10 +36,11 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ -.coverage +.coverage* .cache nosetests.xml coverage.xml +.pytest_cache # Translations *.mo diff --git a/.travis.yml b/.travis.yml index 40f73a8f..ae1b63cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,30 @@ language: python dist: xenial +sudo: false -python: - - 2.7 - - 3.4 - - 3.5 - - 3.6 - - 3.7 + +install: + - pip install tox + +matrix: + include: + - python: '2.7' + env: TOXENV=check27 + - python: '3.6' + env: TOXENV=check36 + - python: '2.7' + env: TOXENV=py27 + - python: '3.4' + env: TOXENV=py34 + - python: '3.5' + env: TOXENV=py35 + - python: '3.6' + env: TOXENV=py36 + - python: '3.7' + env: TOXENV=py37 script: - - python setup.py install - - pip install pytest - - py.test + - tox + +cache: + pip: true diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..50d3cff8 --- /dev/null +++ b/tox.ini @@ -0,0 +1,63 @@ +[tox] +envlist = + check{27,36}, + py{27,py,34,35,36,37}, + coverage + + +[testenv] +usedevelop = true +setenv = + COVERAGE_FILE={toxinidir}/.coverage_{envname} +deps = + pdbpp==0.9.6 + pytest==3.10.1 + pytest-cov==2.6.1 +whitelist_externals = + rm +commands = + rm -vf {toxinidir}/.coverage_{envname} + pytest --cov-report= --cov=obd {posargs} + +[testenv:check27] +basepython = python2.7 +skipsdist = true +deps = + check-manifest==0.37 + flake8==3.7.7 +commands = + flake8 {envsitepackagesdir}/obd + python setup.py check --strict --metadata + + +[testenv:check36] +basepython = python3.6 +skipsdist = true +deps = {[testenv:check27]deps} +commands = {[testenv:check27]commands} + + +[testenv:coverage] +skipsdist = true +deps = + coverage +whitelist_externals = + /bin/bash + rm +commands = + /bin/bash -c 'coverage combine {toxinidir}/.coverage_*' + coverage html -i + coverage report -i --show-missing + +[flake8] +max-line-length = 120 + + +[coverage:run] +omit = + .tox/* + env/* + +[coverage:paths] +source = + obd/ From f21444b0af6c12a5c7546ced1ae33340aad48b86 Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Mon, 25 Feb 2019 21:15:42 -0800 Subject: [PATCH 526/569] elm327: Add low power mode support Add support for entering low power mode. By sending a ATLP command we can tell the ELM327 to enter low power mode. We also add support for leaving low power mode by sending a space. Signed-off-by: Alistair Francis --- obd/elm327.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 4a865a95..dfa00cec 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -56,6 +56,7 @@ class ELM327: """ ELM_PROMPT = b'>' + ELM_LP_ACTIVE = b'OK' _SUPPORTED_PROTOCOLS = { # "0" : None, @@ -116,6 +117,7 @@ def __init__(self, portname, baudrate, protocol, timeout, self.__status = OBDStatus.NOT_CONNECTED self.__port = None self.__protocol = UnknownProtocol([]) + self.__low_power = False self.timeout = timeout # ------------- open port ------------- @@ -362,6 +364,62 @@ def protocol_name(self): def protocol_id(self): return self.__protocol.ELM_ID + def low_power(self): + """ + Enter Low Power mode + + This command causes the ELM327 to shut off all but essential + services. + + The ELM327 can be woken up by a message to the RS232 bus as + well as a few other ways. See the Power Control section in + the ELM327 datasheet for details on other ways to wake up + the chip. + + Returns the status from the ELM327, 'OK' means low power mode + is going to become active. + """ + + if self.__status == OBDStatus.NOT_CONNECTED: + logger.info("cannot enter low power when unconnected") + return None + + lines = self.__send(b"ATLP", delay=1) + + if 'OK' in lines: + logger.debug("Successfully entered low power mode") + self.__low_power = True + else: + logger.debug("Failed to enter low power mode") + + return lines + + def normal_power(self): + """ + Exit Low Power mode + + Send a space to trigger the RS232 to wakeup. + + This will send a space even if we aren't in low power mode as + we want to ensure that we will be able to leave low power mode. + + See the Power Control section in the ELM327 datasheet for details + on other ways to wake up the chip. + + Returns the status from the ELM327. + """ + if self.__status == OBDStatus.NOT_CONNECTED: + logger.info("cannot exit low power when unconnected") + return None + + lines = self.__send(b" ") + + # Assume we woke up + logger.debug("Successfully exited low power mode") + self.__low_power = False + + return lines + def close(self): """ Resets the device, and sets all @@ -393,6 +451,10 @@ def send_and_parse(self, cmd): logger.info("cannot send_and_parse() when unconnected") return None + # Check if we are in low power + if self.__low_power == True: + self.normal_power() + lines = self.__send(cmd) messages = self.__protocol(lines) return messages @@ -466,8 +528,9 @@ def __read(self): buffer.extend(data) - # end on chevron (ELM prompt character) - if self.ELM_PROMPT in buffer: + # end on chevron (ELM prompt character) or an 'OK' which + # indicates we are entering low power state + if self.ELM_PROMPT in buffer or self.ELM_LP_ACTIVE in buffer: break # log, and remove the "bytearray( ... )" part From 6e5564de6a6f9d251dc24f95a1fa2b8b66ab3af1 Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Mon, 25 Feb 2019 21:17:18 -0800 Subject: [PATCH 527/569] obd: Expose low power mode functions Signed-off-by: Alistair Francis --- obd/obd.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/obd/obd.py b/obd/obd.py index 610f13bf..83dec865 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -170,6 +170,20 @@ def status(self): else: return self.interface.status() + def low_power(self): + """ Enter low power mode """ + if self.interface is None: + return OBDStatus.NOT_CONNECTED + else: + return self.interface.low_power() + + def normal_power(self): + """ Exit low power mode """ + if self.interface is None: + return OBDStatus.NOT_CONNECTED + else: + return self.interface.normal_power() + # not sure how useful this would be # def ecus(self): From d2c9291e65ba7745b9cc657225c69eeb83bbfed7 Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Tue, 26 Feb 2019 21:56:34 -0800 Subject: [PATCH 528/569] obd: Allow starting OBD with low power enabled Add support for connecting to an ELM device that is already in low power mode. Signed-off-by: Alistair Francis --- docs/Connections.md | 4 +++- obd/asynchronous.py | 5 +++-- obd/elm327.py | 7 ++++++- obd/obd.py | 13 ++++++++----- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/Connections.md b/docs/Connections.md index d30489a3..4825e608 100644 --- a/docs/Connections.md +++ b/docs/Connections.md @@ -20,7 +20,7 @@ connection = obd.OBD(ports[0]) # connect to the first port in the list
-### OBD(portstr=None, baudrate=None, protocol=None, fast=True, timeout=0.1, check_voltage=True): +### OBD(portstr=None, baudrate=None, protocol=None, fast=True, timeout=0.1, check_voltage=True, start_low_power=False): `portstr`: The UNIX device file or Windows COM Port for your adapter. The default value (`None`) will auto select a port. @@ -39,6 +39,8 @@ Disabling fast mode will guarantee that python-OBD outputs the unaltered command `check_voltage`: Optional argument that is `True` by default and when set to `False` disables the detection of the car supply voltage on OBDII port (which should be about 12V). This control assumes that, if the voltage is lower than 6V, the OBDII port is disconnected from the car. If the option is enabled, it adds the `OBDStatus.OBD_CONNECTED` status, which is set when enough voltage is returned (socket connected to the car) but the ignition is off (no communication with the vehicle). Setting the option to `False` should be needed when the adapter does not support the voltage pin or more generally when the hardware provides unreliable results, or if the pin reads the switched ignition voltage rather than the battery positive (this depends on the car). +`start_low_power`: Optional argument that defaults to `False`. If set to `True` the initial connection will take longer (roughly 1 more second) but will support waking the ELM327 from low power mode before starting the connection. It does this by sending a space to the chip to trigger a charecter being received on the RS232 input line. This is sent before the baud rate is setup, to ensure the device is awake to detect the baud rate. +
--- diff --git a/obd/asynchronous.py b/obd/asynchronous.py index d6f55413..bf70bea2 100644 --- a/obd/asynchronous.py +++ b/obd/asynchronous.py @@ -46,9 +46,10 @@ class Async(OBD): """ def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True, - timeout=0.1, check_voltage=True, delay_cmds=0.25): + timeout=0.1, check_voltage=True, start_low_power=False, + delay_cmds=0.25): super(Async, self).__init__(portstr, baudrate, protocol, fast, - timeout, check_voltage) + timeout, check_voltage, start_low_power) self.__commands = {} # key = OBDCommand, value = Response self.__callbacks = {} # key = OBDCommand, value = list of Functions self.__thread = None diff --git a/obd/elm327.py b/obd/elm327.py index dfa00cec..6b639b9b 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -104,7 +104,7 @@ class ELM327: _TRY_BAUDS = [38400, 9600, 230400, 115200, 57600, 19200] def __init__(self, portname, baudrate, protocol, timeout, - check_voltage=True): + check_voltage=True, start_low_power=False): """Initializes port by resetting device and gettings supported PIDs. """ logger.info("Initializing ELM327: PORT=%s BAUD=%s PROTOCOL=%s" % @@ -134,6 +134,11 @@ def __init__(self, portname, baudrate, protocol, timeout, self.__error(e) return + # If we start with the IC in the low power state we need to wake it up + if start_low_power: + self.__write(b" ") + time.sleep(1) + # ------------------------ find the ELM's baud ------------------------ if not self.set_baudrate(baudrate): diff --git a/obd/obd.py b/obd/obd.py index 83dec865..5bd29e60 100644 --- a/obd/obd.py +++ b/obd/obd.py @@ -50,7 +50,7 @@ class OBD(object): """ def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True, - timeout=0.1, check_voltage=True): + timeout=0.1, check_voltage=True, start_low_power=False): self.interface = None self.supported_commands = set(commands.base_commands()) self.fast = fast # global switch for disabling optimizations @@ -61,11 +61,12 @@ def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True, logger.info("======================= python-OBD (v%s) =======================" % __version__) self.__connect(portstr, baudrate, protocol, - check_voltage) # initialize by connecting and loading sensors + check_voltage, start_low_power) # initialize by connecting and loading sensors self.__load_commands() # try to load the car's supported commands logger.info("===================================================================") - def __connect(self, portstr, baudrate, protocol, check_voltage): + def __connect(self, portstr, baudrate, protocol, check_voltage, + start_low_power): """ Attempts to instantiate an ELM327 connection object. """ @@ -82,14 +83,16 @@ def __connect(self, portstr, baudrate, protocol, check_voltage): for port in port_names: logger.info("Attempting to use port: " + str(port)) self.interface = ELM327(port, baudrate, protocol, - self.timeout, check_voltage) + self.timeout, check_voltage, + start_low_power) if self.interface.status() >= OBDStatus.ELM_CONNECTED: break # success! stop searching for serial else: logger.info("Explicit port defined") self.interface = ELM327(portstr, baudrate, protocol, - self.timeout, check_voltage) + self.timeout, check_voltage, + start_low_power) # if the connection failed, close it if self.interface.status() == OBDStatus.NOT_CONNECTED: From 280a93281a9adc1a6d2f2adb06f1d5931506448f Mon Sep 17 00:00:00 2001 From: Ircama Date: Sun, 10 Mar 2019 11:12:32 +0100 Subject: [PATCH 529/569] Avoiding Async exception on serial port failure **Fix obd.Async() exception when failing to open the serial device.** Fixed exception: AttributeError: 'Async' object has no attribute '_Async__thread' Testing program: import obd import sys connection = obd.Async(sys.argv[1]) **Before the patch:** No exception: python3 test-async.py /dev/existing-device Exception: python3 test-async.py /dev/unexisting-device AttributeError: 'Async' object has no attribute '_Async__thread' **After the patch:** python3 test-async.py /dev/existing-device no exception python3 test-async.py /dev/unexisting-device [obd.elm327] [Errno 2] could not open port /dev/unexisting-device: [Errno 2] No such file or directory: '/dev/unexisting-device' [obd.obd] Cannot load commands: No connection to car --- obd/asynchronous.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/asynchronous.py b/obd/asynchronous.py index bf70bea2..3a8eaf5a 100644 --- a/obd/asynchronous.py +++ b/obd/asynchronous.py @@ -48,11 +48,11 @@ class Async(OBD): def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True, timeout=0.1, check_voltage=True, start_low_power=False, delay_cmds=0.25): + self.__thread = None super(Async, self).__init__(portstr, baudrate, protocol, fast, timeout, check_voltage, start_low_power) self.__commands = {} # key = OBDCommand, value = Response self.__callbacks = {} # key = OBDCommand, value = list of Functions - self.__thread = None self.__running = False self.__was_running = False # used with __enter__() and __exit__() self.__delay_cmds = delay_cmds From 9a08d543ecddb63f1c81347fec335b62988bbd4b Mon Sep 17 00:00:00 2001 From: LightSoar <3341515+LightSoar@users.noreply.github.com> Date: Sun, 17 Mar 2019 12:53:18 -0400 Subject: [PATCH 530/569] adding __repr__ Better representation of a container of `OBDCommand`s --- obd/OBDCommand.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 4b52b59e..6e659e01 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -114,6 +114,9 @@ def __constrain_message_data(self, message): def __str__(self): return "%s: %s" % (self.command, self.desc) + + def __repr(self): + return "OBDCommand(%s, %s)" % (self.name, self.command) def __hash__(self): # needed for using commands as keys in a dict (see async.py) From 45692a7fa2088d4e5d15e45a58339b4fda17c20f Mon Sep 17 00:00:00 2001 From: Ircama Date: Tue, 19 Mar 2019 00:48:08 +0100 Subject: [PATCH 531/569] Fix 'toy-command' class type used for testing It should be 'bytes' type and not string. --- tests/test_OBD.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_OBD.py b/tests/test_OBD.py index 3c9bfb4a..b6348f8b 100644 --- a/tests/test_OBD.py +++ b/tests/test_OBD.py @@ -61,7 +61,7 @@ def _test_last_command(self, expected): # a toy command to test with command = OBDCommand("Test_Command", "A test command", - "0123456789ABCDEF", + b"0123456789ABCDEF", 0, noop, ECU.ALL, From 1d4a0dee3ffc789996e3aa0a6d24a52de195e81c Mon Sep 17 00:00:00 2001 From: Ircama Date: Mon, 11 Mar 2019 00:28:37 +0100 Subject: [PATCH 532/569] Fix bugs related to header management Revise compliance for PEP8 recommendation --- obd/OBDCommand.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 6e659e01..0dc23cce 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -65,7 +65,8 @@ def clone(self): self.bytes, self.decode, self.ecu, - self.fast) + self.fast, + self.header) @property def mode(self): @@ -106,24 +107,30 @@ def __constrain_message_data(self, message): if len(message.data) > self.bytes: # chop off the right side message.data = message.data[:self.bytes] - logger.debug("Message was longer than expected. Trimmed message: " + repr(message.data)) + logger.debug( + "Message was longer than expected. Trimmed message: " + + repr(message.data)) elif len(message.data) < self.bytes: # pad the right with zeros message.data += (b'\x00' * (self.bytes - len(message.data))) - logger.debug("Message was shorter than expected. Padded message: " + repr(message.data)) + logger.debug( + "Message was shorter than expected. Padded message: " + + repr(message.data)) def __str__(self): + if self.header != ECU_HEADER.ENGINE: + return "%s: %s" % (self.header + self.command, self.desc) return "%s: %s" % (self.command, self.desc) - + def __repr(self): return "OBDCommand(%s, %s)" % (self.name, self.command) def __hash__(self): # needed for using commands as keys in a dict (see async.py) - return hash(self.command) + return hash(self.header + self.command) def __eq__(self, other): if isinstance(other, OBDCommand): - return self.command == other.command + return self.command == other.command and self.header == other.header else: return False From 2f3349bf649084b9cc86ae664d3bd33db3bf9721 Mon Sep 17 00:00:00 2001 From: Ircama Date: Sat, 23 Mar 2019 09:19:05 +0100 Subject: [PATCH 533/569] revise __repr__ to produce a working function - revise __repr__ to produce a working function - avoid a one line if statement - Indentation correction - Further revised indentation --- obd/OBDCommand.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index 0dc23cce..d6b8cda5 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -122,8 +122,22 @@ def __str__(self): return "%s: %s" % (self.header + self.command, self.desc) return "%s: %s" % (self.command, self.desc) - def __repr(self): - return "OBDCommand(%s, %s)" % (self.name, self.command) + def __repr__(self): + e = self.ecu + if self.ecu == ECU.ALL: + e = "ECU.ALL" + if self.ecu == ECU.ENGINE: + e = "ECU.ENGINE" + if self.ecu == ECU.TRANSMISSION: + e = "ECU.TRANSMISSION" + if self.header == ECU_HEADER.ENGINE: + return ("OBDCommand(%s, %s, %s, %s, raw_string, ecu=%s, fast=%s)" + ) % (repr(self.name), repr(self.desc), repr(self.command), + self.bytes, e, self.fast) + return ("OBDCommand" + + "(%s, %s, %s, %s, raw_string, ecu=%s, fast=%s, header=%s)" + ) % (repr(self.name), repr(self.desc), repr(self.command), + self.bytes, e, self.fast, repr(self.header)) def __hash__(self): # needed for using commands as keys in a dict (see async.py) From b4304127e22130d02d382cf7ae49e478368731e6 Mon Sep 17 00:00:00 2001 From: Ircama Date: Sun, 7 Apr 2019 23:36:23 +0200 Subject: [PATCH 534/569] Better message size error Adding actual size and expected size in response error messages in case of wrong size. This helps checking the size of custom PIDs (e.g., 4th parameter of OBDCommand). --- obd/OBDCommand.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py index d6b8cda5..b3e1a6ce 100644 --- a/obd/OBDCommand.py +++ b/obd/OBDCommand.py @@ -103,18 +103,21 @@ def __call__(self, messages): def __constrain_message_data(self, message): """ pads or chops the data field to the size specified by this command """ + len_msg_data = len(message.data) if self.bytes > 0: - if len(message.data) > self.bytes: + if len_msg_data > self.bytes: # chop off the right side message.data = message.data[:self.bytes] logger.debug( - "Message was longer than expected. Trimmed message: " + + "Message was longer than expected (%s>%s). " + + "Trimmed message: %s", len_msg_data, self.bytes, repr(message.data)) - elif len(message.data) < self.bytes: + elif len_msg_data < self.bytes: # pad the right with zeros - message.data += (b'\x00' * (self.bytes - len(message.data))) + message.data += (b'\x00' * (self.bytes - len_msg_data)) logger.debug( - "Message was shorter than expected. Padded message: " + + "Message was shorter than expected (%s<%s). " + + "Padded message: %s", len_msg_data, self.bytes, repr(message.data)) def __str__(self): From 1d5813ad222e1e59f174fe4a23be6f32e98ea934 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 14 May 2019 20:54:50 -0700 Subject: [PATCH 535/569] Fix small typo in custom commands docs --- docs/Custom Commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Custom Commands.md b/docs/Custom Commands.md index 0de504c7..466deb29 100644 --- a/docs/Custom Commands.md +++ b/docs/Custom Commands.md @@ -10,7 +10,7 @@ If the command you need is not in python-OBDs tables, you can create a new `OBDC | decoder | callable | Function used for decoding messages from the OBD adapter | | ecu (optional) | ECU | ID of the ECU this command should listen to (`ECU.ALL` by default) | | fast (optional) | bool | Allows python-OBD to alter this command for efficieny (`False` by default) | -| header (optional) | string | If set, use a custom header instead of the defalut one (7E0) | +| header (optional) | string | If set, use a custom header instead of the default one (7E0) | Example From 73855dfef9ed4ec4d6c980e2fe8e0d7d65ece724 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Tue, 14 May 2019 21:51:52 -0700 Subject: [PATCH 536/569] bumped to v0.7.1 --- obd/__version__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/__version__.py b/obd/__version__.py index a71c5c7f..f0788a87 100644 --- a/obd/__version__.py +++ b/obd/__version__.py @@ -1 +1 @@ -__version__ = '0.7.0' +__version__ = '0.7.1' diff --git a/setup.py b/setup.py index 4fdacc11..17a9c7e8 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name="obd", - version="0.7.0", + version="0.7.1", description=("Serial module for handling live sensor data from a vehicle's OBD-II port"), long_description=long_description, long_description_content_type="text/markdown", From bbcacae58bcaf0f93401f86416e369f479129b0f Mon Sep 17 00:00:00 2001 From: Cyrax Date: Mon, 24 Jun 2019 06:41:30 -0600 Subject: [PATCH 537/569] Minor change to the protocol values While the documentation had no quotes, the code needed the quotes. Passing obd.Async(port, protocol="3") is the correct way not obd.Async(port, protocol=3). While this is evident from reading the code, this change makes is more evident. --- docs/Connections.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/Connections.md b/docs/Connections.md index 4825e608..8ffa9051 100644 --- a/docs/Connections.md +++ b/docs/Connections.md @@ -120,16 +120,18 @@ Both functions return string names for the protocol currently being used by the |ID | Name | |---|--------------------------| -| 1 | SAE J1850 PWM | -| 2 | SAE J1850 VPW | -| 3 | AUTO, ISO 9141-2 | -| 4 | ISO 14230-4 (KWP 5BAUD) | -| 5 | ISO 14230-4 (KWP FAST) | -| 6 | ISO 15765-4 (CAN 11/500) | -| 7 | ISO 15765-4 (CAN 29/500) | -| 8 | ISO 15765-4 (CAN 11/250) | -| 9 | ISO 15765-4 (CAN 29/250) | -| A | SAE J1939 (CAN 29/250) | +| "1" | SAE J1850 PWM | +| "2" | SAE J1850 VPW | +| "3" | AUTO, ISO 9141-2 | +| "4" | ISO 14230-4 (KWP 5BAUD) | +| "5" | ISO 14230-4 (KWP FAST) | +| "6" | ISO 15765-4 (CAN 11/500) | +| "7" | ISO 15765-4 (CAN 29/500) | +| "8" | ISO 15765-4 (CAN 11/250) | +| "9" | ISO 15765-4 (CAN 29/250) | +| "A" | SAE J1939 (CAN 29/250) | + +*Note the quotations around the possible IDs* --- From c96ae070d5708c6877d9f5e3246b96b2e9d033ec Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Tue, 30 Jul 2019 20:44:12 -0700 Subject: [PATCH 538/569] README: Add a common issue section Add a common issue section and document the Bluetooth issue. Signed-off-by: Alistair Francis --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 463f35c7..cb386a1d 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,17 @@ Here are a handful of the supported commands (sensors). For a full list, see [th - Hybrid battery pack remaining life - Engine fuel rate +Common Issues +------------- + +### Bluetooth OBD-II Adapters + +There are sometimes connection issues when using a Bluetooth OBD-II adapter with some devices (the Raspberry Pi is a common problem). This can be fixed by setting the following arguments when setting up the connection: + +```Python +fast=False, timeout=30 +``` + License ------- From b799dd6b73b0749f05feb033eba65d8d42b69e80 Mon Sep 17 00:00:00 2001 From: Zane Claes Date: Tue, 29 Oct 2019 12:36:22 -0400 Subject: [PATCH 539/569] Fix send delay for bluetooth/Raspberry Pi --- obd/elm327.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index 6b639b9b..bf782b0c 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -472,14 +472,22 @@ def __send(self, cmd, delay=None): returns result of __read() (a list of line strings) after an optional delay. """ - self.__write(cmd) + delayed = 0.0 if delay is not None: logger.debug("wait: %d seconds" % delay) time.sleep(delay) - - return self.__read() + delayed += delay + + r = self.__read() + while delayed < 1.0 and len(r) <= 0: + d = 0.1 + logger.debug("no response; wait: %f seconds" % d) + time.sleep(d) + delayed += d + r = self.__read() + return r def __write(self, cmd): """ From 069f6aacfc751f5a3ada8e71492cc159fa7c7e32 Mon Sep 17 00:00:00 2001 From: Joseph Curtis Date: Wed, 11 Dec 2019 09:54:41 -0800 Subject: [PATCH 540/569] Patch invalid protocol error. --- obd/elm327.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/obd/elm327.py b/obd/elm327.py index bf782b0c..79e6abff 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -210,7 +210,9 @@ def set_protocol(self, protocol_): if protocol_ is not None: # an explicit protocol was specified if protocol_ not in self._SUPPORTED_PROTOCOLS: - logger.error("%s is not a valid protocol. Please use \"1\" through \"A\"") + logger.error( + "{:} is not a valid protocol. ".format(protocol_) + + "Please use \"1\" through \"A\"") return False return self.manual_protocol(protocol_) else: From 2f5d2fd4680a1d1cdc95831fc852efa07a7fb1de Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Sun, 8 Dec 2019 13:20:14 -0800 Subject: [PATCH 541/569] Add support for Mode 9 PIDS This adds support for the Mode 9 PIDS, including VIN and CVN. This is baed on the original work by Paul Mundt (https://github.com/brendan-w/python-OBD/pull/151). This patch includes adding a few test cases for the new decoders. Signed-off-by: Alistair Francis --- README.md | 1 + docs/Command Tables.md | 22 ++++++++++++++++++++++ obd/commands.py | 23 +++++++++++++++++++++++ obd/decoders.py | 29 +++++++++++++++++++++++++++++ tests/test_decoders.py | 8 ++++++++ 5 files changed, 83 insertions(+) diff --git a/README.md b/README.md index cb386a1d..319594e7 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Here are a handful of the supported commands (sensors). For a full list, see [th - Time since trouble codes cleared - Hybrid battery pack remaining life - Engine fuel rate +- Vehicle Identification Number (VIN) Common Issues ------------- diff --git a/docs/Command Tables.md b/docs/Command Tables.md index 8ea0046f..9b8d6e45 100644 --- a/docs/Command Tables.md +++ b/docs/Command Tables.md @@ -261,3 +261,25 @@ The return value will be encoded in the same structure as the Mode 03 `GET_DTC` | N/A | GET_CURRENT_DTC | Get DTCs from the current/last driving cycle | [special](Responses.md#diagnostic-trouble-codes-dtcs) |
+ +# Mode 09 + +*WARNING: mode 09 is experimental. While it has been tested on a hardware simulator, only a subset of the supported +commands have (00-06) been tested. Any debug output for this mode, especially for the untested PIDs, would be greatly appreciated.* + +|PID | Name | Description | Response Value | +|----|------------------------------|----------------------------------------------------|-----------------------| +| 00 | PIDS_9A | Supported PIDs [01-20] | BitArray | +| 01 | VIN_MESSAGE_COUNT | VIN Message Count | Unit.count | +| 02 | VIN | Vehicle Identification Number | string | +| 03 | CALIBRATION_ID_MESSAGE_COUNT | Calibration ID message count for PID 04 | Unit.count | +| 04 | CALIBRATION_ID | Calibration ID | string | +| 05 | CVN_MESSAGE_COUNT | CVN Message Count for PID 06 | Unit.count | +| 06 | CVN | Calibration Verification Numbers | hex string | +| 07 | PERF_TRACKING_MESSAGE_COUNT | Performance tracking message count | TODO | +| 08 | PERF_TRACKING_SPARK | In-use performance tracking (spark ignition) | TODO | +| 09 | ECU_NAME_MESSAGE_COUNT | ECU Name Message Count for PID 0A | TODO | +| 0a | ECU_NAME | ECU Name | TODO | +| 0b | PERF_TRACKING_COMPRESSION | In-use performance tracking (compression ignition) | TODO | + +
diff --git a/obd/commands.py b/obd/commands.py index 43ec8f3c..9162f1c8 100644 --- a/obd/commands.py +++ b/obd/commands.py @@ -279,6 +279,27 @@ OBDCommand("GET_CURRENT_DTC", "Get DTCs from the current/last driving cycle", b"07", 0, dtc, ECU.ALL, False), ] + +__mode9__ = [ + # name description cmd bytes decoder ECU fast + OBDCommand("PIDS_9A" , "Supported PIDs [01-20]" , b"0900", 7, pid, ECU.ALL, True), + OBDCommand("VIN_MESSAGE_COUNT" , "VIN Message Count" , b"0901", 3, count, ECU.ENGINE, True), + OBDCommand("VIN" , "Vehicle Identification Number" , b"0902", 22, encoded_string(17), ECU.ENGINE, True), + OBDCommand("CALIBRATION_ID_MESSAGE_COUNT","Calibration ID message count for PID 04" , b"0903", 3, count, ECU.ALL, True), + OBDCommand("CALIBRATION_ID" , "Calibration ID" , b"0904", 18, encoded_string(16), ECU.ALL, True), + OBDCommand("CVN_MESSAGE_COUNT" , "CVN Message Count for PID 06" , b"0905", 3, count, ECU.ALL, True), + OBDCommand("CVN" , "Calibration Verification Numbers" , b"0906", 10, cvn, ECU.ALL, True), + +# +# NOTE: The following are untested +# +# OBDCommand("PERF_TRACKING_MESSAGE_COUNT", "Performance tracking message count" , b"0907", 3, count, ECU.ALL, True), +# OBDCommand("PERF_TRACKING_SPARK" , "In-use performance tracking (spark ignition)" , b"0908", 4, raw_string, ECU.ALL, True), +# OBDCommand("ECU_NAME_MESSAGE_COUNT" , "ECU Name Message Count for PID 0A" , b"0909", 3, count, ECU.ALL, True), +# OBDCommand("ECU_NAME" , "ECU Name" , b"090a", 20, raw_string, ECU.ALL, True), +# OBDCommand("PERF_TRACKING_COMPRESSION" , "In-use performance tracking (compression ignition)", b"090b", 4, raw_string, ECU.ALL, True), +] + __misc__ = [ OBDCommand("ELM_VERSION", "ELM327 version string", b"ATI", 0, raw_string, ECU.UNKNOWN, False), OBDCommand("ELM_VOLTAGE", "Voltage detected by OBD-II adapter", b"ATRV", 0, elm_voltage, ECU.UNKNOWN, False), @@ -303,6 +324,7 @@ def __init__(self): __mode6__, __mode7__, [], + __mode9__, ] # allow commands to be accessed by name @@ -350,6 +372,7 @@ def base_commands(self): """ return [ self.PIDS_A, + self.PIDS_9A, self.MIDS_A, self.GET_DTC, self.CLEAR_DTC, diff --git a/obd/decoders.py b/obd/decoders.py index 3020be0c..fffca50a 100644 --- a/obd/decoders.py +++ b/obd/decoders.py @@ -94,6 +94,10 @@ def decode_uas(messages, id_): Return pint Quantities """ +def count(messages): + d = messages[0].data[2:] + v = bytes_to_int(d) + return v * Unit.count # 0 to 100 % def percent(messages): @@ -484,3 +488,28 @@ def monitor(messages): mon.add_test(test) return mon + + +def encoded_string(length): + """ Extract an encoded string from multi-part messages """ + return functools.partial(decode_encoded_string, length=length) + + +def decode_encoded_string(messages, length): + d = messages[0].data[2:] + + if len(d) < length: + logger.debug("Invalid string {}. Discarding...", d) + return None + + # Encoded strings come in bundles of messages with leading null values to + # pad out the string to the next full message size. We strip off the + # leading null characters here and return the resulting string. + return d.strip().strip(b'\x00' b'\x01' b'\x02' b'\\x00' b'\\x01' b'\\x02') + + +def cvn(messages): + d = decode_encoded_string(messages, 4) + if d is None: + return None + return bytes_to_hex(d) diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 9f49b062..062d9655 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -295,6 +295,14 @@ def test_dtc(): ("B0003", ""), ] +def test_vin_message_count(): + assert d.count(m("0901")) == 0 + +def test_vin(): + assert d.encoded_string(17)(m("0201575030" + "5A5A5A39395A54" + "53333932313234")) == bytearray(b'WP0ZZZ99ZTS392124') + +def test_cvn(): + assert d.cvn(m("6021791bc8216e0b")) == '791bc8216e' def test_monitor(): # single test ----------------------------------------- From 74ba2bc673a292a3bf2ea2305b4a7ec43f670680 Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Wed, 26 Feb 2020 22:21:12 -0800 Subject: [PATCH 542/569] travis: Drop support for Python2 Signed-off-by: Alistair Francis --- .travis.yml | 4 ---- tox.ini | 18 ++---------------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index ae1b63cb..54f43108 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,12 +8,8 @@ install: matrix: include: - - python: '2.7' - env: TOXENV=check27 - python: '3.6' env: TOXENV=check36 - - python: '2.7' - env: TOXENV=py27 - python: '3.4' env: TOXENV=py34 - python: '3.5' diff --git a/tox.ini b/tox.ini index 50d3cff8..3812569d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,9 @@ [tox] envlist = - check{27,36}, - py{27,py,34,35,36,37}, + check{36}, + py{34,35,36,37}, coverage - [testenv] usedevelop = true setenv = @@ -19,22 +18,9 @@ commands = rm -vf {toxinidir}/.coverage_{envname} pytest --cov-report= --cov=obd {posargs} -[testenv:check27] -basepython = python2.7 -skipsdist = true -deps = - check-manifest==0.37 - flake8==3.7.7 -commands = - flake8 {envsitepackagesdir}/obd - python setup.py check --strict --metadata - - [testenv:check36] basepython = python3.6 skipsdist = true -deps = {[testenv:check27]deps} -commands = {[testenv:check27]commands} [testenv:coverage] From c6af7e12cab51ea78d16aedd858f67b6465d7722 Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Wed, 26 Feb 2020 22:23:37 -0800 Subject: [PATCH 543/569] travis: Test Python 3.8 Signed-off-by: Alistair Francis --- .travis.yml | 5 ++++- tox.ini | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 54f43108..57c84096 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: python dist: xenial sudo: false - install: - pip install tox @@ -10,6 +9,8 @@ matrix: include: - python: '3.6' env: TOXENV=check36 + - python: '3.8' + env: TOXENV=check38 - python: '3.4' env: TOXENV=py34 - python: '3.5' @@ -18,6 +19,8 @@ matrix: env: TOXENV=py36 - python: '3.7' env: TOXENV=py37 + - python: '3.8' + env: TOXENV=py38 script: - tox diff --git a/tox.ini b/tox.ini index 3812569d..2de0b4a1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = - check{36}, - py{34,35,36,37}, + check{36,38}, + py{34,35,36,37,38}, coverage [testenv] From c14db6a3f13c3b0cf43dc6701d146cc92b08e5eb Mon Sep 17 00:00:00 2001 From: Catalin Ghenea Date: Fri, 10 Jul 2020 03:55:29 +0300 Subject: [PATCH 544/569] Fix ECU ID for legacy protocol #### Issues Addressed When using OBD over legacy protocols like K-line the message is rejected because the ecu id is undefined even if the data is retrieved correctly. #### Summary of changes Add the ENGINE ID and TRANSMISSION ID to the list of ecu map at object creation time to have it available when receiving the message. #### Summary of testing Data is retrieved correctly on K-Line. Car used VW Lupo GTI Data is retrieved correctly on CAN cars. Car used Ford Focus. Signed-off-by: Catalin Ghenea --- obd/protocols/protocol.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py index e677c03d..b07d2f7c 100644 --- a/obd/protocols/protocol.py +++ b/obd/protocols/protocol.py @@ -146,6 +146,12 @@ def __init__(self, lines_0100): # for example: self.TX_ID_ENGINE : ECU.ENGINE self.ecu_map = {} + if (self.TX_ID_ENGINE is not None): + self.ecu_map[self.TX_ID_ENGINE] = ECU.ENGINE + + if (self.TX_ID_TRANSMISSION is not None): + self.ecu_map[self.TX_ID_TRANSMISSION] = ECU.TRANSMISSION + # parse the 0100 data into messages # NOTE: at this point, their "ecu" property will be UNKNOWN messages = self(lines_0100) From d5a0fcd87b2df57cf38dd4ecd4b7b6b9cac018fd Mon Sep 17 00:00:00 2001 From: Ircama Date: Fri, 12 Mar 2021 13:35:06 +0100 Subject: [PATCH 545/569] Support slow OBD adapters Increase "0100" query (PIDS_A) timeout to support inexpensive/slow OBDII adapters. This patch fixes the case in which "SEARCHING" is not shown after "0100", due to slow response of cheap devices. ``` [obd.elm327] write: '0100\r' [obd.elm327] read: b'SEARCHING...\r... ``` It should also fix the case in which the debug mode needs to be active: in fact the debug logs might slow down the query timeout enough to be able to catch the query answer. Reference #205 Reference #200 Reference #187 --- obd/elm327.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/elm327.py b/obd/elm327.py index 79e6abff..96b1b4dd 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -244,7 +244,7 @@ def auto_protocol(self): r = self.__send(b"ATSP0") # -------------- 0100 (first command, SEARCH protocols) -------------- - r0100 = self.__send(b"0100") + r0100 = self.__send(b"0100", delay=1) if self.__has_message(r0100, "UNABLE TO CONNECT"): logger.error("Failed to query protocol 0100: unable to connect") return False From a36cfccf5a58eb5a26f9dc5390162e36336ed07a Mon Sep 17 00:00:00 2001 From: Catalin Ghenea Date: Mon, 21 Jun 2021 04:07:35 +0100 Subject: [PATCH 546/569] Fix protocol auto detect on BT Elm 327 #### Issues addressed Can't auto detect protocol on BT connection even if the elm devices detects the protocol successfully. Add delay after erasing stored protocols so the adapter can read the discover command #### Summary of testing Tested on an Mazda MX5. The protocol can be detected successfully on each run. Tested for about 10 times in a row. Signed-off-by: Catalin Ghenea --- obd/elm327.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/elm327.py b/obd/elm327.py index 96b1b4dd..db11355c 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -241,7 +241,7 @@ def auto_protocol(self): """ # -------------- try the ELM's auto protocol mode -------------- - r = self.__send(b"ATSP0") + r = self.__send(b"ATSP0", delay=1) # -------------- 0100 (first command, SEARCH protocols) -------------- r0100 = self.__send(b"0100", delay=1) From fe6f472c1d49738ee3e77bc99f7fece5370a442a Mon Sep 17 00:00:00 2001 From: Paul Tiedtke Date: Sun, 2 Jan 2022 00:00:23 +0100 Subject: [PATCH 547/569] Fix broken anchor link in connection docs --- docs/Connections.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Connections.md b/docs/Connections.md index 8ffa9051..fe314cdf 100644 --- a/docs/Connections.md +++ b/docs/Connections.md @@ -26,7 +26,7 @@ connection = obd.OBD(ports[0]) # connect to the first port in the list `baudrate`: The baudrate at which to set the serial connection. This can vary from adapter to adapter. Typical values are: 9600, 38400, 19200, 57600, 115200. The default value (`None`) will auto select a baudrate. -`protocol`: Forces python-OBD to use the given protocol when communicating with the adapter. See [protocol_id()](Connections.md/#protocol_id) for possible values. The default value (`None`) will auto select a protocol. +`protocol`: Forces python-OBD to use the given protocol when communicating with the adapter. See [protocol_id()](#protocol_id) for possible values. The default value (`None`) will auto select a protocol. `fast`: Allows commands to be optimized before being sent to the car. Python-OBD currently makes two such optimizations: From a61b3de3b3dd689fc9478954c4ccc14d70dbd215 Mon Sep 17 00:00:00 2001 From: Roman Nyukhalov Date: Thu, 14 Apr 2022 19:04:15 +1000 Subject: [PATCH 548/569] Define CI pipeline using Github Actions --- .github/workflows/ci.yml | 28 ++++++++++++++++++++++++++++ .travis.yml | 29 ----------------------------- tox.ini | 9 ++++++++- 3 files changed, 36 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..e9e764ca --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI Pipeline + +on: + push: + branches: 'master' + pull_request: + branches: '*' + +jobs: + build: + runs-on: ubuntu-18.04 + strategy: + max-parallel: 4 + fail-fast: false + matrix: + python-version: ['3.5', '3.6', '3.7', '3.8'] + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox tox-gh-actions + - name: Test with tox + run: tox \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 57c84096..00000000 --- a/.travis.yml +++ /dev/null @@ -1,29 +0,0 @@ -language: python -dist: xenial -sudo: false - -install: - - pip install tox - -matrix: - include: - - python: '3.6' - env: TOXENV=check36 - - python: '3.8' - env: TOXENV=check38 - - python: '3.4' - env: TOXENV=py34 - - python: '3.5' - env: TOXENV=py35 - - python: '3.6' - env: TOXENV=py36 - - python: '3.7' - env: TOXENV=py37 - - python: '3.8' - env: TOXENV=py38 - -script: - - tox - -cache: - pip: true diff --git a/tox.ini b/tox.ini index 2de0b4a1..ab6217bb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,16 @@ [tox] envlist = check{36,38}, - py{34,35,36,37,38}, + py{35,36,37,38}, coverage +[gh-actions] +python = + 3.5: py35 + 3.6: py36 + 3.7: py37 + 3.8: py38 + [testenv] usedevelop = true setenv = From a08424da2f61f64a123cf9dbabe351bd90348d94 Mon Sep 17 00:00:00 2001 From: Roman Nyukhalov Date: Wed, 27 Apr 2022 19:42:05 +1000 Subject: [PATCH 549/569] Fix test_populate_ecu_map test --- tests/test_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index b2757276..0c6ccc75 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -74,4 +74,4 @@ def test_populate_ecu_map(): # if no messages were received, then the map is empty p = SAE_J1850_PWM([]) - assert len(p.ecu_map) == 0 + assert p.ecu_map[p.TX_ID_ENGINE] == ECU.ENGINE From dbfd379d0b5f5b65dec9d96986e8c045dd4e8f59 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Fri, 20 May 2022 09:51:12 -0400 Subject: [PATCH 550/569] Only treat "OK" as a response terminator when appropriate Right now __read() will stop reading at "OK" even if a ">" is still coming. This can cause the ">" to be seen as the response to the next command, which confuses the initialization sequence, since the initialization sequence expects a very specific set of responses to its commands. This changes __read() so that by default it only treats ">" as the response terminator. When we issue the "ATLP" command to enter low-power mode, we will use "OK" as the response terminator instead, since that's the only time we don't expect to see a prompt. This should fix #226 and should also fix #227. --- obd/elm327.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/obd/elm327.py b/obd/elm327.py index db11355c..b543effc 100644 --- a/obd/elm327.py +++ b/obd/elm327.py @@ -55,7 +55,9 @@ class ELM327: ecus() """ + # chevron (ELM prompt character) ELM_PROMPT = b'>' + # an 'OK' which indicates we are entering low power state ELM_LP_ACTIVE = b'OK' _SUPPORTED_PROTOCOLS = { @@ -391,7 +393,7 @@ def low_power(self): logger.info("cannot enter low power when unconnected") return None - lines = self.__send(b"ATLP", delay=1) + lines = self.__send(b"ATLP", delay=1, end_marker=self.ELM_LP_ACTIVE) if 'OK' in lines: logger.debug("Successfully entered low power mode") @@ -466,13 +468,14 @@ def send_and_parse(self, cmd): messages = self.__protocol(lines) return messages - def __send(self, cmd, delay=None): + def __send(self, cmd, delay=None, end_marker=ELM_PROMPT): """ unprotected send() function will __write() the given string, no questions asked. returns result of __read() (a list of line strings) - after an optional delay. + after an optional delay, until the end marker (by + default, the prompt) is seen """ self.__write(cmd) @@ -482,13 +485,13 @@ def __send(self, cmd, delay=None): time.sleep(delay) delayed += delay - r = self.__read() + r = self.__read(end_marker=end_marker) while delayed < 1.0 and len(r) <= 0: d = 0.1 logger.debug("no response; wait: %f seconds" % d) time.sleep(d) delayed += d - r = self.__read() + r = self.__read(end_marker=end_marker) return r def __write(self, cmd): @@ -512,11 +515,12 @@ def __write(self, cmd): else: logger.info("cannot perform __write() when unconnected") - def __read(self): + def __read(self, end_marker=ELM_PROMPT): """ "low-level" read function - accumulates characters until the prompt character is seen + accumulates characters until the end marker (by + default, the prompt character) is seen returns a list of [/r/n] delimited strings """ if not self.__port: @@ -543,9 +547,8 @@ def __read(self): buffer.extend(data) - # end on chevron (ELM prompt character) or an 'OK' which - # indicates we are entering low power state - if self.ELM_PROMPT in buffer or self.ELM_LP_ACTIVE in buffer: + # end on specified end-marker sequence + if end_marker in buffer: break # log, and remove the "bytearray( ... )" part From d64064d64a1fa05393d9055c92065333b91bfd58 Mon Sep 17 00:00:00 2001 From: Roman Niukhalov Date: Mon, 11 Apr 2022 19:08:09 +0800 Subject: [PATCH 551/569] Update pint version to 0.19.* --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 17a9c7e8..2e542b81 100644 --- a/setup.py +++ b/setup.py @@ -30,5 +30,5 @@ packages=find_packages(), include_package_data=True, zip_safe=False, - install_requires=["pyserial==3.*", "pint==0.7.*"], + install_requires=["pyserial==3.*", "pint==0.19.*"], ) From c7be924a21c5d0be87cce51e3087e8fb21a0ecf2 Mon Sep 17 00:00:00 2001 From: Roman Niukhalov Date: Fri, 3 Jun 2022 09:12:14 +1000 Subject: [PATCH 552/569] Update the python version range to 3.8-3.10 --- .github/workflows/ci.yml | 4 ++-- tox.ini | 17 +++++------------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9e764ca..0bc5b768 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: max-parallel: 4 fail-fast: false matrix: - python-version: ['3.5', '3.6', '3.7', '3.8'] + python-version: ['3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} @@ -25,4 +25,4 @@ jobs: python -m pip install --upgrade pip python -m pip install tox tox-gh-actions - name: Test with tox - run: tox \ No newline at end of file + run: tox diff --git a/tox.ini b/tox.ini index ab6217bb..ffee2943 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,13 @@ [tox] envlist = - check{36,38}, - py{35,36,37,38}, + py{38,39,310}, coverage [gh-actions] python = - 3.5: py35 - 3.6: py36 - 3.7: py37 3.8: py38 + 3.9: py39 + 3.10: py310 [testenv] usedevelop = true @@ -17,19 +15,14 @@ setenv = COVERAGE_FILE={toxinidir}/.coverage_{envname} deps = pdbpp==0.9.6 - pytest==3.10.1 - pytest-cov==2.6.1 + pytest==7.1.2 + pytest-cov==3.0.0 whitelist_externals = rm commands = rm -vf {toxinidir}/.coverage_{envname} pytest --cov-report= --cov=obd {posargs} -[testenv:check36] -basepython = python3.6 -skipsdist = true - - [testenv:coverage] skipsdist = true deps = From c55a7a98f29c427365a35491bee328762bf38a9b Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Tue, 24 Jan 2023 22:28:43 +1000 Subject: [PATCH 553/569] tox.ini: Bump the python and pypi versions Signed-off-by: Alistair Francis --- tox.ini | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/tox.ini b/tox.ini index ffee2943..20476082 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{38,39,310}, + py{38,39,310,311}, coverage [gh-actions] @@ -8,19 +8,17 @@ python = 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 [testenv] usedevelop = true setenv = COVERAGE_FILE={toxinidir}/.coverage_{envname} deps = - pdbpp==0.9.6 - pytest==7.1.2 - pytest-cov==3.0.0 -whitelist_externals = - rm + pdbpp==0.10.3 + pytest==7.2.1 + pytest-cov==4.0.0 commands = - rm -vf {toxinidir}/.coverage_{envname} pytest --cov-report= --cov=obd {posargs} [testenv:coverage] @@ -29,7 +27,6 @@ deps = coverage whitelist_externals = /bin/bash - rm commands = /bin/bash -c 'coverage combine {toxinidir}/.coverage_*' coverage html -i @@ -38,7 +35,6 @@ commands = [flake8] max-line-length = 120 - [coverage:run] omit = .tox/* From 7642ad61da237a79b8fc43bdf39debcdfbde330c Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Tue, 24 Jan 2023 22:33:41 +1000 Subject: [PATCH 554/569] setup.py: Bump pint version Signed-off-by: Alistair Francis --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2e542b81..c0936440 100644 --- a/setup.py +++ b/setup.py @@ -30,5 +30,5 @@ packages=find_packages(), include_package_data=True, zip_safe=False, - install_requires=["pyserial==3.*", "pint==0.19.*"], + install_requires=["pyserial==3.*", "pint==0.20.*"], ) From e952b9172d43c21f5507b08220ec04408ec95a9f Mon Sep 17 00:00:00 2001 From: Mario Niedermeyer <61496163+Mario2407@users.noreply.github.com> Date: Tue, 7 Mar 2023 12:45:49 +0100 Subject: [PATCH 555/569] Fixed printing with brackets --- docs/Async Connections.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Async Connections.md b/docs/Async Connections.md index b4074be1..8250ab04 100644 --- a/docs/Async Connections.md +++ b/docs/Async Connections.md @@ -32,7 +32,7 @@ connection = obd.Async() # a callback that prints every new value to the console def new_rpm(r): - print r.value + print (r.value) connection.watch(obd.commands.RPM, callback=new_rpm) connection.start() From d95554a0f970647b56c4f0563698c087a5866db4 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 9 Jul 2023 20:08:02 -0700 Subject: [PATCH 556/569] bumped to v0.7.2 --- obd/__version__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/__version__.py b/obd/__version__.py index f0788a87..fb9b668f 100644 --- a/obd/__version__.py +++ b/obd/__version__.py @@ -1 +1 @@ -__version__ = '0.7.1' +__version__ = '0.7.2' diff --git a/setup.py b/setup.py index c0936440..a5e30300 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name="obd", - version="0.7.1", + version="0.7.2", description=("Serial module for handling live sensor data from a vehicle's OBD-II port"), long_description=long_description, long_description_content_type="text/markdown", From 7d5cd0559f1ecedf4107484aa66107bc7bbde32a Mon Sep 17 00:00:00 2001 From: green-green-avk <45503261+green-green-avk@users.noreply.github.com> Date: Sun, 9 Jul 2023 15:28:38 -0700 Subject: [PATCH 557/569] Proper `obd.Unit.percent` --- obd/UnitsAndScaling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/UnitsAndScaling.py b/obd/UnitsAndScaling.py index b49650b1..fe0fae4d 100644 --- a/obd/UnitsAndScaling.py +++ b/obd/UnitsAndScaling.py @@ -36,8 +36,8 @@ # export the unit registry Unit = pint.UnitRegistry() -Unit.define("percent = [] = %") Unit.define("ratio = []") +Unit.define("percent = 1e-2 ratio = %") Unit.define("gps = gram / second = GPS = grams_per_second") Unit.define("lph = liter / hour = LPH = liters_per_hour") Unit.define("ppm = count / 1000000 = PPM = parts_per_million") From eb8679eb9a9e7b744a17acfee3cf38a1c80516aa Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 13 Aug 2023 18:43:43 -0700 Subject: [PATCH 558/569] Added readthedocs yaml config file before config files are enforced --- .readthedocs.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..ae5ae2ee --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,5 @@ +version: 2 + +mkdocs: + configuration: mkdocs.yml + fail_on_warning: false From 3ecbf6fd19d91690e3ae32162c0b58ec9667396b Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 13 Aug 2023 18:51:07 -0700 Subject: [PATCH 559/569] Fixed mkdocs pages -> nav config --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index bec2ac65..ad9086bb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,7 +3,7 @@ repo_url: https://github.com/brendan-w/python-OBD repo_name: GitHub extra_javascript: - assets/extra.js -pages: +nav: - 'Getting Started': 'index.md' - 'OBD Connections': 'Connections.md' - 'Command Lookup': 'Command Lookup.md' From d7aad00c270598cf464741bd100c71f54bc52c63 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 13 Aug 2023 18:56:18 -0700 Subject: [PATCH 560/569] Revert docs updates: This is more finicky than I anticipated Moving to a feature branch This reverts commit 3ecbf6fd19d91690e3ae32162c0b58ec9667396b. This reverts commit eb8679eb9a9e7b744a17acfee3cf38a1c80516aa. --- .readthedocs.yaml | 5 ----- mkdocs.yml | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index ae5ae2ee..00000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,5 +0,0 @@ -version: 2 - -mkdocs: - configuration: mkdocs.yml - fail_on_warning: false diff --git a/mkdocs.yml b/mkdocs.yml index ad9086bb..bec2ac65 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,7 +3,7 @@ repo_url: https://github.com/brendan-w/python-OBD repo_name: GitHub extra_javascript: - assets/extra.js -nav: +pages: - 'Getting Started': 'index.md' - 'OBD Connections': 'Connections.md' - 'Command Lookup': 'Command Lookup.md' From d7c781a595e10316084454b16adccff6ee183935 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 13 Aug 2023 18:43:43 -0700 Subject: [PATCH 561/569] Added readthedocs yaml config file before config files are enforced Also fixed some of the doc headers for more modern versions of mkdocs --- .readthedocs.yaml | 14 ++++++++++++++ docs/Async Connections.md | 2 ++ docs/Command Lookup.md | 2 ++ docs/Command Tables.md | 18 ++++++++++-------- docs/Connections.md | 1 + docs/Custom Commands.md | 12 ++++++------ docs/Debug.md | 2 ++ docs/Responses.md | 18 ++++++++++-------- docs/Troubleshooting.md | 5 ++--- docs/index.md | 8 ++++---- docs/requirements.txt | 1 + mkdocs.yml | 2 +- 12 files changed, 55 insertions(+), 30 deletions(-) create mode 100644 .readthedocs.yaml create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..9c6dea9a --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,14 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +python: + install: + - requirements: docs/requirements.txt + +mkdocs: + configuration: mkdocs.yml + fail_on_warning: false diff --git a/docs/Async Connections.md b/docs/Async Connections.md index 8250ab04..d655faff 100644 --- a/docs/Async Connections.md +++ b/docs/Async Connections.md @@ -1,3 +1,5 @@ +# Async Connections + Since the standard `query()` function is blocking, it can be a hazard for UI event loops. To deal with this, python-OBD has an `Async` connection object that can be used in place of the standard `OBD` object. `Async` is a subclass of `OBD`, and therefore inherits all of the standard methods. However, `Async` adds a few in order to control a threaded update loop. This loop will keep the values of your commands up to date with the vehicle. This way, when the user `query`s the car, the latest response is returned immediately. The update loop is controlled by calling `start()` and `stop()`. To subscribe a command for updating, call `watch()` with your requested OBDCommand. Because the update loop is threaded, commands can only be `watch`ed while the loop is `stop`ed. diff --git a/docs/Command Lookup.md b/docs/Command Lookup.md index 43e9ace7..1610657d 100644 --- a/docs/Command Lookup.md +++ b/docs/Command Lookup.md @@ -1,3 +1,5 @@ +# Command Lookup + `OBDCommand`s are objects used to query information from the vehicle. They contain all of the information neccessary to perform the query, and decode the cars response. Python-OBD has [built in tables](Command Tables.md) for the most common commands. They can be looked up by name, or by mode & PID. ```python diff --git a/docs/Command Tables.md b/docs/Command Tables.md index 9b8d6e45..aebaf85d 100644 --- a/docs/Command Tables.md +++ b/docs/Command Tables.md @@ -1,4 +1,6 @@ -# OBD-II adapter (ELM327 commands) +# Commands + +## OBD-II adapter (ELM327 commands) |PID | Name | Description | Response Value | |-----|-------------|-----------------------------------------|-----------------------| @@ -7,7 +9,7 @@
-# Mode 01 +## Mode 01 |PID | Name | Description | Response Value | |----|---------------------------|-----------------------------------------|-----------------------| @@ -110,7 +112,7 @@
-# Mode 02 +## Mode 02 Mode 02 commands are the same as mode 01, but are metrics from when the last DTC occurred (the freeze frame). To access them by name, simple prepend `DTC_` to the Mode 01 command name. @@ -124,7 +126,7 @@ obd.commands.DTC_RPM # the Mode 02 command
-# Mode 03 +## Mode 03 Mode 03 contains a single command `GET_DTC` which requests all diagnostic trouble codes from the vehicle. The response will contain the codes themselves, as well as a description (if python-OBD has one). See the [DTC Responses](Responses.md#diagnostic-trouble-codes-dtcs) section for more details. @@ -135,7 +137,7 @@ Mode 03 contains a single command `GET_DTC` which requests all diagnostic troubl
-# Mode 04 +## Mode 04 |PID | Name | Description | Response Value | |-----|-----------|-----------------------------------------|-----------------------| @@ -143,7 +145,7 @@ Mode 03 contains a single command `GET_DTC` which requests all diagnostic troubl
-# Mode 06 +## Mode 06 *WARNING: mode 06 is experimental. While it passes software tests, it has not been tested on a real vehicle. Any debug output for this mode would be greatly appreciated.* @@ -252,7 +254,7 @@ Mode 06 commands are used to monitor various test results from the vehicle. All
-# Mode 07 +## Mode 07 The return value will be encoded in the same structure as the Mode 03 `GET_DTC` command. @@ -262,7 +264,7 @@ The return value will be encoded in the same structure as the Mode 03 `GET_DTC`
-# Mode 09 +## Mode 09 *WARNING: mode 09 is experimental. While it has been tested on a hardware simulator, only a subset of the supported commands have (00-06) been tested. Any debug output for this mode, especially for the untested PIDs, would be greatly appreciated.* diff --git a/docs/Connections.md b/docs/Connections.md index fe314cdf..1df861fd 100644 --- a/docs/Connections.md +++ b/docs/Connections.md @@ -1,3 +1,4 @@ +# Connections After installing the library, simply `import obd`, and create a new OBD connection object. By default, python-OBD will scan for Bluetooth and USB serial ports (in that order), and will pick the first connection it finds. The port can also be specified manually by passing a connection string to the OBD constructor. You can also use the `scan_serial` helper retrieve a list of connected ports. diff --git a/docs/Custom Commands.md b/docs/Custom Commands.md index 466deb29..e726030d 100644 --- a/docs/Custom Commands.md +++ b/docs/Custom Commands.md @@ -1,3 +1,4 @@ +# Custom Commands If the command you need is not in python-OBDs tables, you can create a new `OBDCommand` object. The constructor accepts the following arguments (each will become a property). @@ -13,8 +14,7 @@ If the command you need is not in python-OBDs tables, you can create a new `OBDC | header (optional) | string | If set, use a custom header instead of the default one (7E0) | -Example -------- +## Example ```python from obd import OBDCommand, Unit @@ -58,7 +58,7 @@ Here are some details on the less intuitive fields of an OBDCommand: --- -### OBDCommand.decoder +## OBDCommand.decoder The `decoder` argument is a function of following form. @@ -83,7 +83,7 @@ def (messages): --- -### OBDCommand.ecu +## OBDCommand.ecu The `ecu` argument is a constant used to filter incoming messages. Some commands may listen to multiple ECUs (such as DTC decoders), where others may only be concerned with the engine (such as RPM). Currently, python-OBD can only distinguish the engine, but this list may be expanded over time: @@ -94,13 +94,13 @@ The `ecu` argument is a constant used to filter incoming messages. Some commands --- -### OBDCommand.fast +## OBDCommand.fast The optional `fast` argument tells python-OBD whether it is safe to append a `"01"` to the end of the command. This will instruct the adapter to return the first response it recieves, rather than waiting for more (and eventually reaching a timeout). This can speed up requests significantly, and is enabled for most of python-OBDs internal commands. However, for unusual commands, it is safest to leave this disabled. --- -### OBDCommand.header +## OBDCommand.header The optional `header` argument tells python-OBD to use a custom header when querying the command. If not set, python-OBD assumes that the default 7E0 header is needed for querying the command. The switch between default and custom header (and vice versa) is automatically done by python-OBD. diff --git a/docs/Debug.md b/docs/Debug.md index 0f929826..5e1df610 100644 --- a/docs/Debug.md +++ b/docs/Debug.md @@ -1,3 +1,5 @@ +# Debug + python-OBD uses python's builtin logging system. By default, it is setup to send output to `stderr` with a level of WARNING. The module's logger can be accessed via the `logger` variable at the root of the module. For instance, to enable console printing of all debug messages, use the following snippet: ```python diff --git a/docs/Responses.md b/docs/Responses.md index 30b992e6..0eef78f9 100644 --- a/docs/Responses.md +++ b/docs/Responses.md @@ -1,3 +1,5 @@ +# Responses + The `query()` function returns `OBDResponse` objects. These objects have the following properties: | Property | Description | @@ -11,7 +13,7 @@ The `query()` function returns `OBDResponse` objects. These objects have the fol --- -### is_null() +## is_null() Use this function to check if a response is empty. Python-OBD will emit empty responses when it is unable to retrieve data from the car. @@ -25,7 +27,7 @@ if not r.is_null(): --- -# Pint Values +## Pint Values The `value` property typically contains a [Pint](http://pint.readthedocs.io/en/latest/) `Quantity` object, but can also hold complex structures (depending on the request). Pint quantities combine a value and unit into a single class, and are used to represent physical values such as "4 seconds", and "88 mph". This allows for consistency when doing math and unit conversions. Pint maintains a registry of units, which is exposed in python-OBD as `obd.Unit`. @@ -71,7 +73,7 @@ import obd --- -# Status +## Status The status command returns information about the Malfunction Indicator Light (check-engine light), the number of trouble codes being thrown, and the type of engine. @@ -111,7 +113,7 @@ Here are all of the tests names that python-OBD reports: --- -# Diagnostic Trouble Codes (DTCs) +## Diagnostic Trouble Codes (DTCs) Each DTC is represented by a tuple containing the DTC code, and a description (if python-OBD has one). For commands that return multiple DTCs, a list is used. @@ -129,7 +131,7 @@ response.value = ("P0104", "Mass or Volume Air Flow Circuit Intermittent") --- -# Fuel Status +## Fuel Status The fuel status is a tuple of two strings, telling the status of the first and second fuel systems. Most cars only have one system, so the second element will likely be an empty string. The possible fuel statuses are: @@ -144,7 +146,7 @@ The fuel status is a tuple of two strings, telling the status of the first and s --- -# Air Status +## Air Status The air status will be one of these strings: @@ -157,7 +159,7 @@ The air status will be one of these strings: --- -# Oxygen Sensors Present +## Oxygen Sensors Present Returns a 2D structure of tuples (representing bank and sensor number), that holds boolean values for sensor presence. @@ -183,7 +185,7 @@ response.value[1][2] == True # Bank 1, Sensor 2 is present ``` --- -# Monitors (Mode 06 Responses) +## Monitors (Mode 06 Responses) All mode 06 commands return `Monitor` objects holding various test results for the requested sensor. A single monitor response can hold multiple tests, in the form of `MonitorTest` objects. The OBD standard defines some tests, but vehicles can always implement custom tests beyond the standard. Here are the standard Test IDs (TIDs) that python-OBD will recognize: diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index f2b9baca..7889e251 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -1,4 +1,3 @@ - # Debug Output If python-OBD is not working properly, the first thing you should do is enable debug output. Add the following line before your connection code to print all of the debug information to your console: @@ -52,7 +51,7 @@ Here are some common logs from python-OBD, and their meanings: ### Unresponsive ELM -``` +```none [obd] ========================== python-OBD (v0.4.0) ========================== [obd] Explicit port defined [obd] Opening serial port '/dev/pts/2' @@ -93,7 +92,7 @@ print ports # ['/dev/ttyUSB0', '/dev/ttyUSB1'] ### Unresponsive Vehicle -``` +```none [obd] ========================== python-OBD (v0.4.0) ========================== [obd] Explicit port defined [obd] Opening serial port '/dev/pts/2' diff --git a/docs/index.md b/docs/index.md index 7688edea..3d2c1e69 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,7 +6,7 @@ Python-OBD is a library for handling data from a car's [**O**n-**B**oard **D**ia
-# Installation +## Installation Install the latest release from pypi: @@ -22,7 +22,7 @@ $ sudo apt-get install bluetooth bluez-utils blueman
-# Basic Usage +## Basic Usage ```python import obd @@ -41,7 +41,7 @@ OBD connections operate in a request-reply fashion. To retrieve data from the ca
-# Module Layout +## Module Layout ```python import obd @@ -59,7 +59,7 @@ obd.logger # the OBD module's root logger (for debug)
-# License +## License GNU General Public License V2 diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..49ec98c5 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +mkdocs==1.5.2 diff --git a/mkdocs.yml b/mkdocs.yml index bec2ac65..ad9086bb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,7 +3,7 @@ repo_url: https://github.com/brendan-w/python-OBD repo_name: GitHub extra_javascript: - assets/extra.js -pages: +nav: - 'Getting Started': 'index.md' - 'OBD Connections': 'Connections.md' - 'Command Lookup': 'Command Lookup.md' From e0304c3209ac8eb3e75cf0ad5f7afdd6612d28e3 Mon Sep 17 00:00:00 2001 From: Ion Mironov <97184530+ion-mironov@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:47:27 -0500 Subject: [PATCH 562/569] Several spelling and grammatical fixes. --- docs/Command Lookup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Command Lookup.md b/docs/Command Lookup.md index 1610657d..f87ea39a 100644 --- a/docs/Command Lookup.md +++ b/docs/Command Lookup.md @@ -1,6 +1,6 @@ # Command Lookup -`OBDCommand`s are objects used to query information from the vehicle. They contain all of the information neccessary to perform the query, and decode the cars response. Python-OBD has [built in tables](Command Tables.md) for the most common commands. They can be looked up by name, or by mode & PID. +`OBDCommand`s are objects used to query information from the vehicle. They contain all of the information necessary to perform the query and decode the car's response. Python-OBD has [built in tables](Command Tables.md) for the most common commands. They can be looked up by name or by mode & PID. ```python import obd From 0c9abbc4ca9c2aa685e324b383400f47bcf11b34 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 5 Feb 2025 22:07:37 -0500 Subject: [PATCH 563/569] Fix escaped serial port label string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this fix, Python 3.13.1 yields this warning during "import odb": …/obd/utils.py:177: SyntaxWarning: invalid escape sequence '\C' I'm not a Windows expert, but the correct naming convention seems to be described here: https://support.microsoft.com/en-us/topic/howto-specify-serial-ports-larger-than-com9-db9078a5-b7b6-bf00-240f-f749ebfd913e --- obd/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obd/utils.py b/obd/utils.py index 77cb36e2..221d3e9c 100644 --- a/obd/utils.py +++ b/obd/utils.py @@ -174,7 +174,7 @@ def scan_serial(): possible_ports += glob.glob("/dev/ttyUSB[0-9]*") elif sys.platform.startswith('win'): - possible_ports += ["\\.\COM%d" % i for i in range(256)] + possible_ports += [r"\\.\COM%d" % i for i in range(256)] elif sys.platform.startswith('darwin'): exclude = [ From a47d791de1884da167a4bb51f6c576dc07019a9a Mon Sep 17 00:00:00 2001 From: Ion Mironov <97184530+ion-mironov@users.noreply.github.com> Date: Tue, 20 Feb 2024 18:02:27 -0500 Subject: [PATCH 564/569] Included note mentioning which OBD it will work with. --- docs/index.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index 3d2c1e69..890ffd1a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,8 @@ # Welcome -Python-OBD is a library for handling data from a car's [**O**n-**B**oard **D**iagnostics port](https://en.wikipedia.org/wiki/On-board_diagnostics) (OBD-II). It can stream real time sensor data, perform diagnostics (such as reading check-engine codes), and is fit for the Raspberry Pi. This library is designed to work with standard [ELM327 OBD-II adapters](http://www.amazon.com/s/ref=nb_sb_noss?field-keywords=elm327). +Python-OBD is a library for handling data from a car's [**O**n-**B**oard **D**iagnostics](https://en.wikipedia.org/wiki/On-board_diagnostics) port. Please keep in mind that the car **must** have OBD-II (any car made in 1996 and up); this will _**not**_ work with OBD-I. + +Python-OBD can stream real time sensor data, perform diagnostics (such as reading check-engine codes), and is fit for the Raspberry Pi. This library is designed to work with standard [ELM327 OBD-II adapters](http://www.amazon.com/s/ref=nb_sb_noss?field-keywords=elm327). *NOTE: Python-OBD is below 1.0.0, meaning the API may change between minor versions. Consult the [GitHub release page](https://github.com/brendan-w/python-OBD/releases) for changelogs before updating.* @@ -31,13 +33,13 @@ connection = obd.OBD() # auto-connects to USB or RF port cmd = obd.commands.SPEED # select an OBD command (sensor) -response = connection.query(cmd) # send the command, and parse the response +response = connection.query(cmd) # send the command and parse the response print(response.value) # returns unit-bearing values thanks to Pint print(response.value.to("mph")) # user-friendly unit conversions ``` -OBD connections operate in a request-reply fashion. To retrieve data from the car, you must send commands that query for the data you want (e.g. RPM, Vehicle speed, etc). In python-OBD, this is done with the `query()` function. The commands themselves are represented as objects, and can be looked up by name or value in `obd.commands`. The `query()` function will return a response object with parsed data in its `value` property. +OBD connections operate in a request-reply fashion. To retrieve data from the car, you must send commands that query for the data you want (e.g. RPM, Vehicle speed, etc). In python-OBD this is done with the `query()` function. The commands themselves are represented as objects and can be looked up by name or value in `obd.commands`. The `query()` function will return a response object with parsed data in its `value` property.
From 0c019019fddfae2435209fbf8b3d2c53483647a6 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 6 Apr 2025 20:24:33 -0700 Subject: [PATCH 565/569] Uprev pint to 0.24.* for python 3.13+ support --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a5e30300..ac3274c0 100644 --- a/setup.py +++ b/setup.py @@ -30,5 +30,5 @@ packages=find_packages(), include_package_data=True, zip_safe=False, - install_requires=["pyserial==3.*", "pint==0.20.*"], + install_requires=["pyserial==3.*", "pint==0.24.*"], ) From c30cfc3511122f7519582559805d2e87c677dba5 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 6 Apr 2025 21:00:20 -0700 Subject: [PATCH 566/569] Uprev tox.ini to test python 3.13 Also corrected some of the package classifiers to indicate that this python library no longer supports python 2. It didn't even with the older 0.20.* Pint, though Pint itself now only supports python 3.9+ --- setup.py | 6 +++++- tox.ini | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index ac3274c0..f16a0232 100644 --- a/setup.py +++ b/setup.py @@ -16,8 +16,12 @@ "Operating System :: POSIX :: Linux", "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", "Topic :: System :: Monitoring", - "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Development Status :: 3 - Alpha", "Topic :: System :: Logging", "Intended Audience :: Developers", diff --git a/tox.ini b/tox.ini index 20476082..6fe55f43 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,15 @@ [tox] envlist = - py{38,39,310,311}, + py{39,310,311,312,313}, coverage [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 3.11: py311 + 3.12: py312 + 3.13: py313 [testenv] usedevelop = true From 630f0be2709d6ddba6c2a215d05f9b4024db4a21 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 6 Apr 2025 21:20:42 -0700 Subject: [PATCH 567/569] setup.py -> pyproject.toml --- pyproject.toml | 36 ++++++++++++++++++++++++++++++++++++ setup.py | 38 -------------------------------------- 2 files changed, 36 insertions(+), 38 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..2a89a0a7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[project] +name = "obd" +version = "0.7.2" +authors = [ + { name="Brendan Whitfield", email="me@brendan-w.com" }, + { name="Alistair Francis", email="alistair@alistair23.me" }, + { name="Paul Bartek" }, + { name="Peter Harris" }, +] +description = "Serial module for handling live sensor data from a vehicle's OBD-II port" +readme = "README.md" +requires-python = ">=3.9" +classifiers = [ + "Operating System :: POSIX :: Linux", + "Topic :: System :: Monitoring", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Development Status :: 3 - Alpha", + "Topic :: System :: Logging", + "Intended Audience :: Developers", +] +keywords = ["obd", "obdii", "obd-ii", "obd2", "car", "serial", "vehicle", "diagnostic"] +dependencies = [ + "pyserial==3.*", + "pint==0.24.*", +] +license = "GPL-2.0-only" +license-files = ["LICENSE"] + +[project.urls] +Homepage = "https://github.com/brendan-w/python-OBD" +Issues = "https://github.com/brendan-w/python-OBD/issues" diff --git a/setup.py b/setup.py deleted file mode 100644 index f16a0232..00000000 --- a/setup.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/env python -# -*- coding: utf-8 -*- - -from setuptools import setup, find_packages - -with open("README.md", "r") as readme: - long_description = readme.read() - -setup( - name="obd", - version="0.7.2", - description=("Serial module for handling live sensor data from a vehicle's OBD-II port"), - long_description=long_description, - long_description_content_type="text/markdown", - classifiers=[ - "Operating System :: POSIX :: Linux", - "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", - "Topic :: System :: Monitoring", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Development Status :: 3 - Alpha", - "Topic :: System :: Logging", - "Intended Audience :: Developers", - ], - keywords="obd obdii obd-ii obd2 car serial vehicle diagnostic", - author="Brendan Whitfield", - author_email="brendanw@windworksdesign.com", - url="http://github.com/brendan-w/python-OBD", - license="GNU GPLv2", - packages=find_packages(), - include_package_data=True, - zip_safe=False, - install_requires=["pyserial==3.*", "pint==0.24.*"], -) From 3d1beba44f7202f1bd3c6dd7a2deaf3dcd21cee6 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 6 Apr 2025 21:44:37 -0700 Subject: [PATCH 568/569] Updated the testing docs with modern build commands --- tests/README.md | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tests/README.md b/tests/README.md index 2ecfa72e..5f9c4573 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,10 +1,34 @@ Testing ======= -To test python-OBD, you will need to `pip install pytest` and install the module (preferably in a virtualenv) by running `python setup.py install`. The end-to-end tests will also require [obdsim](http://icculus.org/obdgpslogger/obdsim.html) to be running in the background. When starting obdsim, note the "SimPort name" that it creates, and pass it as an argument to py.test. +To test python-OBD, you will need to install `pytest` and the `obd` module from your local tree (preferably in a virtualenv) by running: -To run all tests, run the following command: +```bash +pip install pytest +pip install build +python -m build +pip install ./dist/obd-0.7.2.tar.gz +``` - $ py.test --port=/dev/pts/ +To run all basic python-only unit tests, run: + +```bash +py.test +``` + +This directory also contains a set of end-to-end tests that require [obdsim](http://icculus.org/obdgpslogger/obdsim.html) to be running in the background. These tests are skipped by default, but can be activated by passing the `--port` flag. + +- Download `obdgpslogger`: https://icculus.org/obdgpslogger/downloads/obdgpslogger-0.16.tar.gz +- Run the following build commands: + ```bash + mkdir build + cd build + cmake .. + make obdsim + ``` +- Start `./bin/obdsim`, note the `SimPort name: /dev/pts/` that it creates. Pass this pseudoterminal path as an argument to py.test: + ```bash + py.test --port=/dev/pts/ + ``` For more information on pytest with virtualenvs, [read more here](https://pytest.org/dev/goodpractises.html) \ No newline at end of file From b53cbcbf9e6252461e276d73a7ff75adb714dc53 Mon Sep 17 00:00:00 2001 From: Brendan Whitfield Date: Sun, 6 Apr 2025 22:12:25 -0700 Subject: [PATCH 569/569] bumped to 0.7.3 --- obd/__version__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/obd/__version__.py b/obd/__version__.py index fb9b668f..1ef13195 100644 --- a/obd/__version__.py +++ b/obd/__version__.py @@ -1 +1 @@ -__version__ = '0.7.2' +__version__ = '0.7.3' diff --git a/pyproject.toml b/pyproject.toml index 2a89a0a7..f91d6e3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "obd" -version = "0.7.2" +version = "0.7.3" authors = [ { name="Brendan Whitfield", email="me@brendan-w.com" }, { name="Alistair Francis", email="alistair@alistair23.me" },