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 |
diff --git a/docs/Connections.md b/docs/Connections.md
index be55f7ac..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,18 +12,36 @@ 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
```
+
+
+
+### 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)*
@@ -36,24 +54,92 @@ 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 ELM327 and 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_id()
+### protocol_name()
+
+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 |
+|---|--------------------------|
+| 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) |
+
+---
+
+
+
### close()
Closes the connection.
diff --git a/docs/Custom Commands.md b/docs/Custom Commands.md
index a5952138..150cb412 100644
--- a/docs/Custom Commands.md
+++ b/docs/Custom Commands.md
@@ -1,42 +1,102 @@
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 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) |
-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
```
-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.
+By default, custom commands will be treated as "unsupported by the vehicle". There are two ways to handle this:
-For example:
+```python
+# use the `force` parameter when querying
+o = obd.OBD()
+o.query(c, force=True)
+```
+
+or
```python
-from obd import OBDCommand
-from obd.utils import unhex
+# 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:
-def rpm(_hex):
- v = unhex(_hex) # helper function to convert hex to int
- v = v / 4.0
- return (v, obd.Unit.RPM)
+---
+
+### OBDCommand.decoder
+
+The `decoder` argument is a function of following form.
-c = OBDCommand("RPM", "Engine RPM", "01", "0C", 2, rpm)
+```python
+def ():
+ ...
+ 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.
+
+*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 (, )
```
+*You can also access the original string sent by the adapter using the `Message.raw()` function.*
+
+---
+
+### 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/docs/Troubleshooting.md b/docs/Troubleshooting.md
index 324d9366..716d3f3d 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/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
diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py
index 4c20cbc0..65a365e5 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) #
# #
########################################################################
# #
@@ -29,73 +29,97 @@
# #
########################################################################
-import re
from .utils import *
from .debug import debug
+from .protocols import ECU
+from .OBDResponse import OBDResponse
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 __init__(self,
+ name,
+ desc,
+ command,
+ _bytes,
+ decoder,
+ ecu=ECU.ALL,
+ 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
+ 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)
def clone(self):
return OBDCommand(self.name,
self.desc,
- self.mode,
- self.pid,
+ self.command,
self.bytes,
- self.decode)
+ self.decode,
+ self.ecu,
+ self.fast)
+
+ @property
+ def mode_int(self):
+ if len(self.command) >= 2:
+ return unhex(self.command[:2])
+ else:
+ return 0
+
+ @property
+ def pid_int(self):
+ if len(self.command) > 2:
+ return unhex(self.command[2:])
+ else:
+ return 0
- def get_command(self):
- return self.mode + self.pid # the actual command transmitted to the port
+ # TODO: remove later
+ @property
+ def supported(self):
+ debug("OBDCommand.supported is deprecated. Use OBD.supports() instead", True)
+ return False
- def get_mode_int(self):
- return unhex(self.mode)
+ def __call__(self, messages):
- def get_pid_int(self):
- return unhex(self.pid)
+ # filter for applicable messages (from the right ECU(s))
+ for_us = lambda m: self.ecu & m.ecu > 0
+ messages = list(filter(for_us, messages))
- def __call__(self, message):
+ # 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 = 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]
+ r = OBDResponse(self, messages)
+ 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 """
+ 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 += (b'\x00' * (self.bytes - len(message.data)))
+
+
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/OBDResponse.py b/obd/OBDResponse.py
new file mode 100644
index 00000000..59148f72
--- /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 2016 Brendan Whitfield (brendan-w.com) #
+# #
+########################################################################
+# #
+# OBDResponse.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, 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)
+
+
+
+"""
+ 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/__init__.py b/obd/__init__.py
index 2c5c9e6a..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/brendan-w/python-OBD/wiki
+ http://python-obd.readthedocs.org/en/latest/
"""
########################################################################
@@ -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) #
# #
########################################################################
# #
@@ -38,8 +38,10 @@
from .__version__ import __version__
from .obd import OBD
-from .OBDCommand import OBDCommand
+from .async import Async
from .commands import commands
-from .utils import scanSerial, Unit
+from .OBDCommand import OBDCommand
+from .OBDResponse import OBDResponse, Unit
+from .protocols import ECU
+from .utils import scan_serial, scanSerial, OBDStatus # TODO: scanSerial() deprecated
from .debug import debug
-from .async import Async
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/obd/async.py b/obd/async.py
index 57e1ae0d..264600da 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) #
# #
########################################################################
# #
@@ -31,7 +31,7 @@
import time
import threading
-from .utils import Response
+from .OBDResponse import OBDResponse
from .debug import debug
from . import OBD
@@ -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
@@ -133,14 +133,14 @@ 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
# 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
@@ -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..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) #
# #
########################################################################
# #
@@ -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/commands.py b/obd/commands.py
index 09cb0236..e166d4a5 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) #
# #
########################################################################
# #
@@ -29,6 +29,7 @@
# #
########################################################################
+from .protocols import ECU
from .OBDCommand import OBDCommand
from .decoders import *
from .debug import debug
@@ -40,111 +41,113 @@
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])
+# see OBDCommand.py for descriptions & purposes for each of these fields
+
__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 ),
+ # 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),
+
+ # 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, 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),
]
@@ -152,31 +155,37 @@
__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)
__mode3__ = [
- # sensor name description mode cmd bytes decoder
- OBDCommand("GET_DTC" , "Get DTCs" , "03", "" , 0, dtc , True),
+ # name description cmd bytes decoder ECU fast
+ OBDCommand("GET_DTC" , "Get DTCs" , "03", 0, dtc, ECU.ALL, False),
]
__mode4__ = [
- # sensor name description mode cmd bytes decoder
- OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", "" , 0, noop , True),
+ # name description cmd bytes decoder ECU fast
+ OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", 0, drop, ECU.ALL, False),
]
__mode7__ = [
- # sensor name description mode cmd bytes decoder
- OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", "" , 0, dtc , True),
+ # name description cmd bytes decoder ECU fast
+ OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", 0, dtc, ECU.ALL, False),
+]
+
+__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),
]
'''
-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():
@@ -194,11 +203,14 @@ 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
+ for c in __misc__:
+ self.__dict__[c.name] = c
+
def __getitem__(self, key):
"""
@@ -230,6 +242,21 @@ 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.ELM_VERSION,
+ self.ELM_VOLTAGE,
+ ]
+
+
def pid_getters(self):
""" returns a list of PID GET commands """
getters = []
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 a04a44c1..f6767b1a 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) #
# #
########################################################################
# #
@@ -33,181 +33,225 @@
from .utils import *
from .codes import *
from .debug import debug
+from .OBDResponse import Unit, Status, Test
'''
All decoders take the form:
-def (_hex):
+def ():
...
return (, )
'''
-# todo
-def todo(_hex):
- return (_hex, Unit.NONE)
+# 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, hex out
-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)
+# returns the raw strings from the ELM
+def raw_string(messages):
+ return ("\n".join([m.raw() for m in messages]), Unit.NONE)
+
'''
Sensor decoders
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)
# -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)
# 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)
# 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
-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)
+def elm_voltage(messages):
+ # doesn't register as a normal OBD response,
+ # so access the raw frame data
+ v = messages[0].frames[0].raw
+
+ try:
+ return (float(v), Unit.VOLT)
+ except ValueError:
+ debug("Failed to parse ELM voltage", True)
+ return (None, Unit.NONE)
+
+
'''
Special decoders
Return objects, lists, etc
@@ -215,8 +259,9 @@ def fuel_rate(_hex):
-def status(_hex):
- bits = bitstring(_hex, 32)
+def status(messages):
+ d = messages[0].data
+ bits = bytes_to_bits(d)
output = Status()
output.MIL = bitToBool(bits[0])
@@ -261,8 +306,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)
@@ -283,8 +329,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)
@@ -306,7 +353,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"
@@ -317,7 +365,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"
@@ -327,38 +376,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:
+ # 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 = single_dtc( (d[n-1], d[n]) )
+ 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/elm327.py b/obd/elm327.py
index 5b1f8a25..062ac097 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) #
# #
########################################################################
# #
@@ -33,25 +33,30 @@
import serial
import time
from .protocols import *
-from .utils import numBitsSet
+from .utils import OBDStatus
from .debug import debug
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:
send_and_parse()
- get_port_name()
- is_connected()
close()
+ status()
+ port_name()
+ protocol_name()
+ ecus()
"""
_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,
@@ -66,25 +71,39 @@ class ELM327:
#"C" : None, # user defined 2
}
- def __init__(self, portname, baudrate):
+ # 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
+ ]
+
+
+ def __init__(self, portname, baudrate, protocol):
"""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 -------------
+ self.__status = OBDStatus.NOT_CONNECTED
+ self.__port = None
+ self.__protocol = UnknownProtocol([])
- 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
+ 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:
self.__error(e)
@@ -93,8 +112,6 @@ def __init__(self, portname, baudrate):
self.__error(e)
return
- debug("Serial port successfully opened on " + self.get_port_name())
-
# ---------------------------- ATZ (reset) ----------------------------
try:
@@ -104,184 +121,196 @@ 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
+ # by now, we've successfuly communicated with the ELM, but not the car
+ self.__status = OBDStatus.ELM_CONNECTED
- # ---------------------- ATSPA8 (protocol AUTO) -----------------------
- r = self.__send("ATSPA8")
- if not self.__isok(r):
- self.__error("ATSPA8 did not return 'OK'")
- return
+ # 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")
+ else:
+ debug("Connected to the adapter, but failed to connect to the vehicle", True)
- # -------------- 0100 (first command, SEARCH protocols) --------------
- # TODO: rewrite this using a "wait for prompt character"
- # rather than a fixed wait period
+ 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" % protocol)
r0100 = self.__send("0100")
+ if not self.__has_message(r0100, "UNABLE TO CONNECT"):
+ # success, found the protocol
+ self.__protocol = self._SUPPORTED_PROTOCOLS[protocol](r0100)
+ return True
+
+ return False
+
+
+ def auto_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")
+
+ # -------------- 0100 (first command, SEARCH protocols) --------------
+ r0100 = self.__send("0100")
# ------------------- ATDPN (list protocol number) -------------------
r = self.__send("ATDPN")
+ if len(r) != 1:
+ debug("Failed to retrieve current protocol", True)
+ return False
- if not r:
- self.__error("Describe protocol command didn't return ")
- return
-
- 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:
- self.__error("ELM responded with unknown protocol")
- return
-
- # instantiate the correct protocol handler
- self.__protocol = self._SUPPORTED_PROTOCOLS[p]()
+ # 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")
- # Now that a protocol has been selected, we can figure out
- # which ECU is the primary.
+ for p in self._TRY_PROTOCOL_ORDER:
+ 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
- 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
+ # if we've come this far, then we have failed...
+ return False
- # ------------------------------- 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'
+ # 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'
- 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 __has_message(self, lines, text):
+ for line in lines:
+ if text in line:
+ return True
+ return 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)
+
+ def port_name(self):
if self.__port is not None:
- self.__port.close()
+ return self.__port.portstr
+ else:
+ return "No Port"
+
- self.__connected = False
+ def status(self):
+ return self.__status
- def get_port_name(self):
- return self.__port.portstr if (self.__port is not None) else "No Port"
+ def ecus(self):
+ return self.__protocol.ecu_map.values()
- def is_connected(self):
- return self.__connected and (self.__port is not None)
+ 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 clears all attributes to unconnected state
+ Resets the device, and sets all
+ attributes to unconnected states.
"""
- if self.is_connected():
+ self.__status = OBDStatus.NOT_CONNECTED
+ self.__protocol = None
+
+ if self.__port is not None:
self.__write("ATZ")
self.__port.close()
+ self.__port = None
- self.__connected = False
- self.__port = None
- self.__protocol = None
- 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.
+
+ An empty command string will re-trigger the previous command
- Returns the Message object from the primary ECU, or None,
- if no appropriate response was recieved.
+ Returns a list of Message objects
"""
- if not self.is_connected():
+ if self.__status == OBDStatus.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
+ 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):
@@ -289,7 +318,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)
@@ -335,10 +365,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/obd.py b/obd/obd.py
index 4d057111..a8b509c8 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) #
# #
########################################################################
# #
@@ -29,79 +29,62 @@
# #
########################################################################
-import time
from .__version__ import __version__
from .elm327 import ELM327
from .commands import commands
-from .utils import scanSerial, Response
+from .OBDResponse import OBDResponse
+from .utils import scan_serial, OBDStatus
from .debug import debug
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):
+ 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
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.
- Upon success, __load_commands() is called
"""
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:
+ debug("No OBD-II adapters found", True)
+ return
+
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.is_connected():
- # success! stop searching for serial
- break
+ if self.port.status >= OBDStatus.ELM_CONNECTED:
+ 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.is_connected():
- self.__load_commands()
- else:
- debug("Failed to connect")
-
+ self.port = ELM327(portstr, baudrate, protocol)
- 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"
+ # if the connection failed, close it
+ if self.port.status == OBDStatus.NOT_CONNECTED:
+ # the ELM327 class will report its own errors
+ self.close()
def __load_commands(self):
@@ -110,43 +93,115 @@ def __load_commands(self):
and compiles a list of command objects.
"""
- debug("querying for supported PIDs (commands)...")
-
- self.supported_commands = []
+ if self.status() != OBDStatus.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
if not self.supports(get):
continue
- response = self.__send(get) # ask nicely
+ # 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
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":
- 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]
- c.supported = True
+ self.supported_commands.add(commands[mode][pid])
- # don't add PID getters to the command list
- if c not in pid_getters:
- self.supported_commands.append(c)
+ # 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))
+ def close(self):
+ """
+ Closes the connection, and clears supported_commands
+ """
+
+ self.supported_commands = []
+
+ if self.port is not None:
+ debug("Closing connection")
+ self.port.close()
+ self.port = None
+
+
+ def status(self):
+ """ returns the OBD connection status """
+ if self.port is None:
+ return OBDStatus.NOT_CONNECTED
+ else:
+ return self.port.status()
+
+
+ # 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):
+ """ returns the name of the protocol being used by the ELM327 """
+ if self.port is None:
+ return ""
+ else:
+ 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")
+ 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()
+ 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 = OBDStatus.ELM_CONNECTED
+ """
+ return self.status() == OBDStatus.CAR_CONNECTED
+
+
def print_commands(self):
"""
Utility function meant for working in interactive mode.
@@ -156,41 +211,55 @@ def print_commands(self):
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 supports(self, cmd):
+ """
+ Returns a boolean for whether the given command
+ is supported by the car
+ """
+ return cmd in self.supported_commands
- 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() == OBDStatus.NOT_CONNECTED:
debug("Query failed, no connection available", True)
- return Response() # return empty response
+ return OBDResponse()
+
+ if not force and not self.supports(cmd):
+ debug("'%s' is not supported" % str(cmd), True)
+ return OBDResponse()
- debug("Sending command: %s" % str(c))
# send command and retrieve message
- m = self.port.send_and_parse(c.get_command())
+ debug("Sending command: %s" % str(cmd))
+ cmd_string = self.__build_command_string(cmd)
+ messages = self.port.send_and_parse(cmd_string)
- if m is None:
- return Response() # return empty response
- else:
- return c(m) # compute a response object
+ # 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()
- def query(self, c, force=False):
- """
- primary API function. Sends commands to the car, and
- protects against sending unsupported commands.
- """
+ return cmd(messages) # compute a response object
- # 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
+
+ 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()))
+
+ # if we sent this last time, just send
+ if self.fast and (cmd_string == self.__last_command):
+ cmd_string = ""
+
+ return cmd_string
diff --git a/obd/protocols/README.md b/obd/protocols/README.md
index 15f02a18..7f6a250f 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. 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_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
@@ -40,6 +55,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..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) #
# #
########################################################################
# #
@@ -29,6 +29,10 @@
# #
########################################################################
+from .protocol import ECU
+
+from .protocol_unknown import UnknownProtocol
+
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 e38f75f8..efe83a9a 100644
--- a/obd/protocols/protocol.py
+++ b/obd/protocols/protocol.py
@@ -29,7 +29,8 @@
# #
########################################################################
-from obd.utils import ascii_to_bytes, isHex
+from binascii import hexlify
+from obd.utils import isHex, num_bits_set
from obd.debug import debug
@@ -40,28 +41,60 @@
"""
+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
+ ENGINE = 0b00000010
+ TRANSMISSION = 0b00000100
+
+
class Frame(object):
+ """ represents a single parsed line of OBD output """
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 = 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
class Message(object):
- def __init__(self, frames, tx_id):
- self.frames = frames
- self.tx_id = tx_id
- self.data_bytes = []
+ """ 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()
+
+ @property
+ def tx_id(self):
+ if len(self.frames) == 0:
+ return None
+ else:
+ return self.frames[0].tx_id
+
+ 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 __eq__(self, other):
if isinstance(other, Message):
- for attr in ["frames", "tx_id", "data_bytes"]:
+ for attr in ["frames", "ecu", "data"]:
if getattr(self, attr) != getattr(other, attr):
return False
return True
@@ -73,76 +106,194 @@ def __eq__(self, other):
"""
-Protocol objects are stateless factories for Frames and Messages.
-They are __called__ with the raw string response, 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
+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.
"""
class Protocol(object):
- PRIMARY_ECU = None
+ # 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")
+
+ # the TX_IDs of known ECUs
+ 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.
+ """
+
+ # create the default, empty map
+ # for example: self.TX_ID_ENGINE : ECU.ENGINE
+ self.ecu_map = {}
- def __init__(self, baud=38400):
- self.baud = baud
+ # parse the 0100 data into messages
+ # NOTE: at this point, their "ecu" property will be UNKNOWN
+ 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
- # ditch spaces
- lines = [line.replace(' ', '') for line in lines]
+ accepts a list of raw strings from the car, split by lines
+ """
- # ditch frames without valid hex (trashes "NO DATA", etc...)
- lines = filter(isHex, 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 = []
- frames = []
for line in lines:
- # subclass function to parse the lines into Frames
- frame = self.create_frame(line)
+ line_no_spaces = line.replace(' ', '')
+
+ 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 obd_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)
- ecus = {}
+
+ # group frames by transmitting ECU
+ # 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:
- # subclass function to assemble frames into Messages
- message = self.create_message(ecus[ecu], ecu)
+ for ecu in frames_by_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(frames_by_ECU[ecu])
+
+ # subclass function to assemble frames into Messages
+ if self.parse_message(message):
+ # mark with the appropriate ECU ID
+ message.ecu = self.ecu_map.get(ecu, ECU.UNKNOWN)
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([ Frame(line) ]) )
+
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.
+
+ 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 = [ m for m in messages if m.parsed() ]
+
+ # populate the map
+ if len(messages) == 0:
+ pass
+ elif len(messages) == 1:
+ # if there's only one response, mark it as the engine regardless
+ self.ecu_map[messages[0].tx_id] = ECU.ENGINE
+ else:
+
+ # 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
+
+ for message in messages:
+ bits = sum([num_bits_set(b) for b in message.data])
+
+ if bits > best:
+ best = bits
+ tx_id = message.tx_id
+
+ 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):
"""
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, and the Frame will be 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, and the Message will be dropped.
"""
raise NotImplementedError()
diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py
index 890fa6d6..de2b1319 100644
--- a/obd/protocols/protocol_can.py
+++ b/obd/protocols/protocol_can.py
@@ -29,32 +29,55 @@
# #
########################################################################
+from binascii import unhexlify
from obd.utils import contiguous
from .protocol import *
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):
+ # 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 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
+
+ # 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
- frame = Frame(raw)
- raw_bytes = ascii_to_bytes(raw)
+ raw_bytes = bytearray(unhexlify(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:
@@ -83,48 +106,57 @@ def create_frame(self, raw):
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_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
+ # 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,
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
+ # 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
- frame.data_len = (frame.data_bytes[0] & 0x0F) << 8
- frame.data_len += frame.data_bytes[1]
+ # 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:
# 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]
+ # [ 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:
# sort FF and CF into their own lists
@@ -143,15 +175,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:]):
@@ -170,36 +202,69 @@ def create_message(self, frames, tx_id):
# 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 None
+ 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_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 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_bytes[0]
+ 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_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:]
+ # 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 message
+ return True
##############################################
@@ -211,25 +276,35 @@ 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)
+ 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):
- def __init__(self):
- CANProtocol.__init__(self, baud=500000, id_bits=29)
+ 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):
- def __init__(self):
- CANProtocol.__init__(self, baud=250000, id_bits=11)
+ 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):
- def __init__(self):
- CANProtocol.__init__(self, baud=250000, id_bits=29)
+ 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):
- def __init__(self):
- CANProtocol.__init__(self, baud=250000, id_bits=29)
+ 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 1de6378f..82599303 100644
--- a/obd/protocols/protocol_legacy.py
+++ b/obd/protocols/protocol_legacy.py
@@ -29,29 +29,33 @@
# #
########################################################################
+from binascii import unhexlify
from obd.utils import contiguous
from .protocol import *
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)
- frame = Frame(raw)
- raw_bytes = ascii_to_bytes(raw)
+
+ def parse_frame(self, frame):
+
+ raw = frame.raw
+
+ raw_bytes = bytearray(unhexlify(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 +63,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 +100,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 +111,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 +124,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 +149,35 @@ def create_message(self, frames, tx_id):
class SAE_J1850_PWM(LegacyProtocol):
- def __init__(self):
- LegacyProtocol.__init__(self, baud=41600)
+ ELM_NAME = "SAE J1850 PWM"
+ ELM_ID = "1"
+ def __init__(self, lines_0100):
+ LegacyProtocol.__init__(self, lines_0100)
class SAE_J1850_VPW(LegacyProtocol):
- def __init__(self):
- LegacyProtocol.__init__(self, baud=10400)
+ ELM_NAME = "SAE J1850 VPW"
+ ELM_ID = "2"
+ def __init__(self, lines_0100):
+ LegacyProtocol.__init__(self, lines_0100)
class ISO_9141_2(LegacyProtocol):
- def __init__(self):
- LegacyProtocol.__init__(self, baud=10400)
+ ELM_NAME = "ISO 9141-2"
+ ELM_ID = "3"
+ def __init__(self, lines_0100):
+ LegacyProtocol.__init__(self, lines_0100)
class ISO_14230_4_5baud(LegacyProtocol):
- def __init__(self):
- LegacyProtocol.__init__(self, baud=10400)
+ 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):
- def __init__(self):
- LegacyProtocol.__init__(self, baud=10400)
+ 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
new file mode 100644
index 00000000..69fac01a
--- /dev/null
+++ b/obd/protocols/protocol_unknown.py
@@ -0,0 +1,49 @@
+
+########################################################################
+# #
+# 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 parse_frame(self, frame):
+ return True # pass everything
+
+ def parse_message(self, message):
+ return True # pass everything
diff --git a/obd/utils.py b/obd/utils.py
index c98fa6f7..ea3b3cd3 100644
--- a/obd/utils.py
+++ b/obd/utils.py
@@ -32,87 +32,23 @@
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 = []
- for i in range(0, len(a), 2):
- b.append(int(a[i:i+2], 16))
- return b
+class OBDStatus:
+ """ Values for the connection status flags """
+
+ NOT_CONNECTED = "Not Connected"
+ ELM_CONNECTED = "ELM Connected"
+ CAR_CONNECTED = "Car Connected"
+
+
-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
@@ -121,6 +57,29 @@ 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 bytes_to_hex(bs):
+ h = ""
+ for b in bs:
+ bh = hex(b)[2:]
+ h += ("0" * (2 - len(bh))) + bh
+ return h
+
def bitstring(_hex, bits=None):
b = bin(unhex(_hex))[2:]
if bits is not None:
@@ -137,23 +96,10 @@ def twos_comp(val, num_bits):
return val
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
+ return all([c in string.hexdigits for c in _hex])
- 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
-
-# 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:
@@ -185,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 = []
@@ -210,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()
diff --git a/setup.py b/setup.py
index 5e8ce5a8..3b79abdc 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",
@@ -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/brendan-w/python-OBD",
diff --git a/tests/README.md b/tests/README.md
index e264e0c7..2ecfa72e 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 --port=/dev/pts/
-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
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 00000000..b4216842
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,6 @@
+
+import pytest
+
+def pytest_addoption(parser):
+ parser.addoption("--port", action="store", default=None,
+ help="device file for doing end-to-end testing")
diff --git a/tests/test_OBD.py b/tests/test_OBD.py
index 90d70c11..35b22b81 100644
--- a/tests/test_OBD.py
+++ b/tests/test_OBD.py
@@ -1,101 +1,187 @@
+"""
+ Tests for the API layer
+"""
+
import obd
-from obd.utils import Response
-from obd.commands import OBDCommand
+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 import SAE_J1850_PWM
+
+
+
+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
+ self._status = OBDStatus.CAR_CONNECTED
+ self._last_command = None
+
+ def port_name(self):
+ return self._portname
+
+ def status(self):
+ return self._status
+
+ 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 = bytearray(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
+ self._last_command = None
+ return r
+
+
+# a toy command to test with
+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()
-
- # 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", "01", "23", 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__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
+ o = obd.OBD("/dev/null")
+ assert not o.is_connected()
+
+ # our fake ELM class always returns success for connections
+ o.port = FakeELM("/dev/null")
+ assert o.is_connected()
+
+
+def test_status():
+ """
+ Make sure that the API's status() function reports the
+ same values as the underlying ELM327 class.
+ """
+ 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")
+
+ 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")
+
+ # since we haven't actually connected,
+ # no commands should be marked as supported
+ assert not o.supports(obd.commands.RPM)
+ 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
+ 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")
+ assert o.port_name() == o.port._portname
+
+ 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")
+
+ 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")
+
+ 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)
diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py
index d1788cb8..b0bb4aa7 100644
--- a/tests/test_OBDCommand.py
+++ b/tests/test_OBDCommand.py
@@ -1,80 +1,84 @@
-from obd.commands import OBDCommand
+from obd.OBDCommand import OBDCommand
+from obd.OBDResponse import Unit
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)
- 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
+ # 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"
+ assert cmd.command == "0123"
+ assert cmd.bytes == 2
+ assert cmd.decode == noop
+ assert cmd.ecu == ECU.ENGINE
+ assert cmd.fast == 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
+ cmd = OBDCommand("Test 2", "example OBD command", "0123", 2, noop, ECU.ENGINE, True)
+ assert cmd.fast == True
- cmd = OBDCommand("Test", "example OBD command", "01", "23", 2, noop, True)
- assert cmd.supported == True
def test_clone():
- # name description mode cmd bytes decoder
- cmd = OBDCommand("", "", "01", "23", 2, noop)
- 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.fast == cmd.fast
- 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_call():
- p = SAE_J1850_PWM()
- m = 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("", "", "01", "23", 4, noop)
- r = cmd(m[0])
- assert r.value == "BE1FB811"
+ print(messages[0].data)
- # response too short (pad)
- cmd = OBDCommand("", "", "01", "23", 5, noop)
- r = cmd(m[0])
- assert r.value == "BE1FB81100"
+ # valid response size
+ cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE)
+ r = cmd(messages)
+ assert r.value == b'\xBE\x1F\xB8\x11'
- # response too long (clip)
- cmd = OBDCommand("", "", "01", "23", 3, noop)
- r = cmd(m[0])
- assert r.value == "BE1FB8"
+ # response too short (pad)
+ 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, noop, ECU.ENGINE)
+ r = cmd(messages)
+ assert r.value == b'\xBE\x1F\xB8'
-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("", "", "0123", 4, noop, ECU.ENGINE)
+ assert cmd.mode_int == 0x01
- cmd = OBDCommand("", "", "", "23", 4, noop)
- assert cmd.get_mode_int() == 0
+ cmd = OBDCommand("", "", "", "23", 4, noop, ECU.ENGINE)
+ assert cmd.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
+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.pid_int == 0
diff --git a/tests/test_commands.py b/tests/test_commands.py
index e1cad593..0ac0df2f 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -4,79 +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):
- # 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 cmd.command != "", "The Command's command string must not be null"
- # 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 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"
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.get_mode_int()
- pid = cmd.get_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.get_mode_int()
- pid = cmd.get_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 d8fc9310..cd6b783f 100644
--- a/tests/test_decoders.py
+++ b/tests/test_decoders.py
@@ -1,164 +1,197 @@
-from obd.utils import Unit
+from binascii import unhexlify
+
+from obd.OBDResponse import Unit
+from obd.protocols.protocol import Frame, Message
import obd.decoders as d
+# returns a list with a single valid message,
+# containing the requested data
+def m(hex_data, frames=[]):
+ # most decoders don't look at the underlying frame objects
+ message = Message(frames)
+ message.data = bytearray(unhexlify(hex_data))
+ return [message]
+
+
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(m("00010203")) == (bytearray([0, 1, 2, 3]), Unit.NONE)
+
+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("00000000") == ("00000000000000000000000000000000", Unit.NONE)
- assert d.pid("F00AA00F") == ("11110000000010101010000000001111", Unit.NONE)
- assert d.pid("11") == ("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)
- 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_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("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(m("0104")) == ([
+ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"),
+ ], Unit.NONE)
+
+ # multiple codes
+ 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")) == ([
+ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"),
+ ("B0003", "Unknown error code"),
+ ], Unit.NONE)
+
+ # 0000 codes are dropped
+ 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)
diff --git a/tests/test_elm327.py b/tests/test_elm327.py
index 83c0ed4a..781befc6 100644
--- a/tests/test_elm327.py
+++ b/tests/test_elm327.py
@@ -1,25 +1,4 @@
-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
-
- # 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
-
- # 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
-
- # if no messages were received, no ECU could be determined
- assert p._ELM327__find_primary_ecu([]) == None
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()
+ '''
+"""
diff --git a/tests/test_obdsim.py b/tests/test_obdsim.py
new file mode 100644
index 00000000..287ade6c
--- /dev/null
+++ b/tests/test_obdsim.py
@@ -0,0 +1,150 @@
+
+import time
+import pytest
+from obd import commands, Unit
+
+STANDARD_WAIT_TIME = 0.25
+
+
+@pytest.fixture(scope="module")
+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)
+
+
+@pytest.fixture(scope="module")
+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 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(good_rpm_response(r))
+
+
+# Async tests
+
+def test_async_query(async):
+
+ rs = []
+ async.watch(commands.RPM)
+ async.start()
+
+ for i in range(5):
+ 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)
+ 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(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.stop()
+ async.unwatch_all()
+
+ 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
new file mode 100644
index 00000000..a7f95a1a
--- /dev/null
+++ b/tests/test_protocol.py
@@ -0,0 +1,81 @@
+
+import random
+from obd.utils import unhex
+from obd.protocols import *
+from obd.protocols.protocol import Frame, Message
+
+
+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_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
+
+
+def test_message():
+
+ # constructor
+ 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
+
+
+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
+
+ # 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[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[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[0x12] == ECU.ENGINE
+
+ # if no messages were received, then the map is empty
+ p = SAE_J1850_PWM([])
+ assert len(p.ecu_map) == 0
diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py
index 8799f1f6..222ec2c6 100644
--- a/tests/test_protocol_can.py
+++ b/tests/test_protocol_can.py
@@ -3,147 +3,185 @@
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,
- 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_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 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)
+def test_single_frame():
+ for protocol in CAN_11_PROTOCOLS:
+ p = protocol([])
-def test_single_frame():
- 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)))
+ 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():
- for protocol in CAN_11_PROTOCOLS:
- p = protocol()
+ """
+ If non-hex values are sent, they should be marked as ECU.UNKNOWN
+ """
+ for protocol in CAN_11_PROTOCOLS:
+ p = protocol([])
- r = p(["NO DATA"])
- assert len(r) == 0
+ # 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(["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)))
+ # multiple non-hex message
+ r = p(["12.8 Volts", "NO DATA"])
+ assert len(r) == 2
- r = p(["NO DATA", "NO DATA"])
- 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) == 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)))
+
+ # 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 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",
- ]
+def test_multi_ecu():
+ for protocol in CAN_11_PROTOCOLS:
+ p = protocol([])
- correct_data = list(range(4))
- # seperate ECUs, single frames each
- r = p(test_case)
- assert len(r) == 3
+ 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",
+ ]
- # 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)
+ correct_data = list(range(4))
+ # 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)
def test_multi_line():
- for protocol in CAN_11_PROTOCOLS:
- p = protocol()
+ """
+ Tests that valid multiline messages are recombined into single
+ messages.
+ """
+
+ 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"
+ ]
+
+ 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)
+ # 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_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))
- # in-order
- 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
+ """
- # 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)
+ 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"
+ ]
- # missing frames in a multi-frame message should drop the message
- # (tests the contiguity check, and data length byte)
+ for n in range(len(test_case) - 1):
+ sub_test = list(test_case)
+ del sub_test[n]
- 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(sub_test)
+ assert len(r) == 0
- 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
+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.
+ """
- # MODE 03 COMMANDS (GET_DTC) RETURN NO PID BYTE
+ for protocol in CAN_11_PROTOCOLS:
+ p = protocol([])
- 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 f828967a..37631f71 100644
--- a/tests/test_protocol_legacy.py
+++ b/tests/test_protocol_legacy.py
@@ -3,145 +3,181 @@
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
+ 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 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)
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():
- for protocol in LEGACY_PROTOCOLS:
- p = protocol()
+ """
+ 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
+ # 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
- r = p(["NO DATA", "NO DATA"])
- assert len(r) == 0
+ # mixed hex and non-hex
+ r = p(["NO DATA", "48 6B 10 41 00 00 01 02 03 FF"])
+ assert len(r) == 2
- r = p(["NO DATA", "48 6B 10 41 00 00 01 02 03 FF"])
- assert len(r) == 1
- 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
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():
- for protocol in LEGACY_PROTOCOLS:
- p = protocol()
+ """
+ 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",
+ "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)
- 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))
+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
+ """
- # in-order
- r = p(test_case)
- assert len(r) == 1
- check_message(r[0], len(test_case), 0x10, correct_data)
+ for protocol in LEGACY_PROTOCOLS:
+ p = protocol([])
- # 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_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",
+ ]
- # missing frames in a multi-frame message should drop the message
- # (tests the contiguity check, and data length byte)
+ for n in range(len(test_case) - 1):
+ sub_test = list(test_case)
+ del sub_test[n]
- 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",
- ]
+ r = p(sub_test)
+ assert len(r) == 0
- 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
+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([])
- # 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",
- ]
+ 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)