diff --git a/README.md b/README.md
index 3bc11a5a..bf54aa1c 100644
--- a/README.md
+++ b/README.md
@@ -20,12 +20,12 @@ import obd
connection = obd.OBD() # auto-connects to USB or RF port
-cmd = obd.commands.RPM # select an OBD command (sensor)
+cmd = obd.commands.SPEED # select an OBD command (sensor)
response = connection.query(cmd) # send the command, and parse the response
-print(response.value)
-print(response.unit)
+print(response.value) # returns unit-bearing values thanks to Pint
+print(response.value.to("mph")) # user-friendly unit conversions
```
Documentation
diff --git a/docs/Command Lookup.md b/docs/Command Lookup.md
new file mode 100644
index 00000000..43e9ace7
--- /dev/null
+++ b/docs/Command Lookup.md
@@ -0,0 +1,59 @@
+`OBDCommand`s are objects used to query information from the vehicle. They contain all of the information neccessary to perform the query, and decode the cars response. Python-OBD has [built in tables](Command Tables.md) for the most common commands. They can be looked up by name, or by mode & PID.
+
+```python
+import obd
+
+c = obd.commands.RPM
+
+# OR
+
+c = obd.commands['RPM']
+
+# OR
+
+c = obd.commands[1][12] # mode 1, PID 12 (RPM)
+```
+
+The `commands` table also has a few helper methods for determining if a particular name or PID is present.
+
+---
+
+### has_command(command)
+
+Checks the internal command tables for the existance of the given `OBDCommand` object. Commands are compared by mode and PID value.
+
+```python
+import obd
+obd.commands.has_command(obd.commands.RPM) # True
+```
+
+---
+
+### has_name(name)
+
+Checks the internal command tables for a command with the given name. This is also the function of the `in` operator.
+
+```python
+import obd
+
+obd.commands.has_name('RPM') # True
+
+# OR
+
+'RPM' in obd.commands # True
+```
+
+---
+
+### has_pid(mode, pid)
+
+Checks the internal command tables for a command with the given mode and PID.
+
+```python
+import obd
+obd.commands.has_pid(1, 12) # True
+```
+
+---
+
+
diff --git a/docs/Command Tables.md b/docs/Command Tables.md
new file mode 100644
index 00000000..f48ffa1a
--- /dev/null
+++ b/docs/Command Tables.md
@@ -0,0 +1,263 @@
+# OBD-II adapter (ELM327 commands)
+
+|PID | Name | Description | Response Value |
+|-----|-------------|-----------------------------------------|-----------------------|
+| N/A | ELM_VERSION | OBD-II adapter version string | string |
+| N/A | ELM_VOLTAGE | Voltage detected by OBD-II adapter | Unit.volt |
+
+
+
+# Mode 01
+
+|PID | Name | Description | Response Value |
+|----|---------------------------|-----------------------------------------|-----------------------|
+| 00 | PIDS_A | Supported PIDs [01-20] | bitarray |
+| 01 | STATUS | Status since DTCs cleared | [special](Responses.md#status) |
+| 02 | FREEZE_DTC | DTC that triggered the freeze frame | [special](Responses.md#diagnostic-trouble-codes-dtcs) |
+| 03 | FUEL_STATUS | Fuel System Status | [(string, string)](Responses.md#fuel-status) |
+| 04 | ENGINE_LOAD | Calculated Engine Load | Unit.percent |
+| 05 | COOLANT_TEMP | Engine Coolant Temperature | Unit.celsius |
+| 06 | SHORT_FUEL_TRIM_1 | Short Term Fuel Trim - Bank 1 | Unit.percent |
+| 07 | LONG_FUEL_TRIM_1 | Long Term Fuel Trim - Bank 1 | Unit.percent |
+| 08 | SHORT_FUEL_TRIM_2 | Short Term Fuel Trim - Bank 2 | Unit.percent |
+| 09 | LONG_FUEL_TRIM_2 | Long Term Fuel Trim - Bank 2 | Unit.percent |
+| 0A | FUEL_PRESSURE | Fuel Pressure | Unit.kilopascal |
+| 0B | INTAKE_PRESSURE | Intake Manifold Pressure | Unit.kilopascal |
+| 0C | RPM | Engine RPM | Unit.rpm |
+| 0D | SPEED | Vehicle Speed | Unit.kph |
+| 0E | TIMING_ADVANCE | Timing Advance | Unit.degree |
+| 0F | INTAKE_TEMP | Intake Air Temp | Unit.celsius |
+| 10 | MAF | Air Flow Rate (MAF) | Unit.grams_per_second |
+| 11 | THROTTLE_POS | Throttle Position | Unit.percent |
+| 12 | AIR_STATUS | Secondary Air Status | [string](Responses.md#air-status) |
+| 13 | O2_SENSORS | O2 Sensors Present | [special](Responses.md#oxygen-sensors-present) |
+| 14 | O2_B1S1 | O2: Bank 1 - Sensor 1 Voltage | Unit.volt |
+| 15 | O2_B1S2 | O2: Bank 1 - Sensor 2 Voltage | Unit.volt |
+| 16 | O2_B1S3 | O2: Bank 1 - Sensor 3 Voltage | Unit.volt |
+| 17 | O2_B1S4 | O2: Bank 1 - Sensor 4 Voltage | Unit.volt |
+| 18 | O2_B2S1 | O2: Bank 2 - Sensor 1 Voltage | Unit.volt |
+| 19 | O2_B2S2 | O2: Bank 2 - Sensor 2 Voltage | Unit.volt |
+| 1A | O2_B2S3 | O2: Bank 2 - Sensor 3 Voltage | Unit.volt |
+| 1B | O2_B2S4 | O2: Bank 2 - Sensor 4 Voltage | Unit.volt |
+| 1C | OBD_COMPLIANCE | OBD Standards Compliance | string |
+| 1D | O2_SENSORS_ALT | O2 Sensors Present (alternate) | [special](Responses.md#oxygen-sensors-present) |
+| 1E | AUX_INPUT_STATUS | Auxiliary input status (power take off) | boolean |
+| 1F | RUN_TIME | Engine Run Time | Unit.second |
+| 20 | PIDS_B | Supported PIDs [21-40] | bitarray |
+| 21 | DISTANCE_W_MIL | Distance Traveled with MIL on | Unit.kilometer |
+| 22 | FUEL_RAIL_PRESSURE_VAC | Fuel Rail Pressure (relative to vacuum) | Unit.kilopascal |
+| 23 | FUEL_RAIL_PRESSURE_DIRECT | Fuel Rail Pressure (direct inject) | Unit.kilopascal |
+| 24 | O2_S1_WR_VOLTAGE | 02 Sensor 1 WR Lambda Voltage | Unit.volt |
+| 25 | O2_S2_WR_VOLTAGE | 02 Sensor 2 WR Lambda Voltage | Unit.volt |
+| 26 | O2_S3_WR_VOLTAGE | 02 Sensor 3 WR Lambda Voltage | Unit.volt |
+| 27 | O2_S4_WR_VOLTAGE | 02 Sensor 4 WR Lambda Voltage | Unit.volt |
+| 28 | O2_S5_WR_VOLTAGE | 02 Sensor 5 WR Lambda Voltage | Unit.volt |
+| 29 | O2_S6_WR_VOLTAGE | 02 Sensor 6 WR Lambda Voltage | Unit.volt |
+| 2A | O2_S7_WR_VOLTAGE | 02 Sensor 7 WR Lambda Voltage | Unit.volt |
+| 2B | O2_S8_WR_VOLTAGE | 02 Sensor 8 WR Lambda Voltage | Unit.volt |
+| 2C | COMMANDED_EGR | Commanded EGR | Unit.percent |
+| 2D | EGR_ERROR | EGR Error | Unit.percent |
+| 2E | EVAPORATIVE_PURGE | Commanded Evaporative Purge | Unit.percent |
+| 2F | FUEL_LEVEL | Fuel Level Input | Unit.percent |
+| 30 | WARMUPS_SINCE_DTC_CLEAR | Number of warm-ups since codes cleared | Unit.count |
+| 31 | DISTANCE_SINCE_DTC_CLEAR | Distance traveled since codes cleared | Unit.kilometer |
+| 32 | EVAP_VAPOR_PRESSURE | Evaporative system vapor pressure | Unit.pascal |
+| 33 | BAROMETRIC_PRESSURE | Barometric Pressure | Unit.kilopascal |
+| 34 | O2_S1_WR_CURRENT | 02 Sensor 1 WR Lambda Current | Unit.milliampere |
+| 35 | O2_S2_WR_CURRENT | 02 Sensor 2 WR Lambda Current | Unit.milliampere |
+| 36 | O2_S3_WR_CURRENT | 02 Sensor 3 WR Lambda Current | Unit.milliampere |
+| 37 | O2_S4_WR_CURRENT | 02 Sensor 4 WR Lambda Current | Unit.milliampere |
+| 38 | O2_S5_WR_CURRENT | 02 Sensor 5 WR Lambda Current | Unit.milliampere |
+| 39 | O2_S6_WR_CURRENT | 02 Sensor 6 WR Lambda Current | Unit.milliampere |
+| 3A | O2_S7_WR_CURRENT | 02 Sensor 7 WR Lambda Current | Unit.milliampere |
+| 3B | O2_S8_WR_CURRENT | 02 Sensor 8 WR Lambda Current | Unit.milliampere |
+| 3C | CATALYST_TEMP_B1S1 | Catalyst Temperature: Bank 1 - Sensor 1 | Unit.celsius |
+| 3D | CATALYST_TEMP_B2S1 | Catalyst Temperature: Bank 2 - Sensor 1 | Unit.celsius |
+| 3E | CATALYST_TEMP_B1S2 | Catalyst Temperature: Bank 1 - Sensor 2 | Unit.celsius |
+| 3F | CATALYST_TEMP_B2S2 | Catalyst Temperature: Bank 2 - Sensor 2 | Unit.celsius |
+| 40 | PIDS_C | Supported PIDs [41-60] | bitarray |
+| 41 | STATUS_DRIVE_CYCLE | Monitor status this drive cycle | [special](Responses.md#status) |
+| 42 | CONTROL_MODULE_VOLTAGE | Control module voltage | Unit.volt |
+| 43 | ABSOLUTE_LOAD | Absolute load value | Unit.percent |
+| 44 | COMMANDED_EQUIV_RATIO | Commanded equivalence ratio | Unit.ratio |
+| 45 | RELATIVE_THROTTLE_POS | Relative throttle position | Unit.percent |
+| 46 | AMBIANT_AIR_TEMP | Ambient air temperature | Unit.celsius |
+| 47 | THROTTLE_POS_B | Absolute throttle position B | Unit.percent |
+| 48 | THROTTLE_POS_C | Absolute throttle position C | Unit.percent |
+| 49 | ACCELERATOR_POS_D | Accelerator pedal position D | Unit.percent |
+| 4A | ACCELERATOR_POS_E | Accelerator pedal position E | Unit.percent |
+| 4B | ACCELERATOR_POS_F | Accelerator pedal position F | Unit.percent |
+| 4C | THROTTLE_ACTUATOR | Commanded throttle actuator | Unit.percent |
+| 4D | RUN_TIME_MIL | Time run with MIL on | Unit.minute |
+| 4E | TIME_SINCE_DTC_CLEARED | Time since trouble codes cleared | Unit.minute |
+| 4F | *unsupported* | *unsupported* | |
+| 50 | MAX_MAF | Maximum value for mass air flow sensor | Unit.grams_per_second |
+| 51 | FUEL_TYPE | Fuel Type | string |
+| 52 | ETHANOL_PERCENT | Ethanol Fuel Percent | Unit.percent |
+| 53 | EVAP_VAPOR_PRESSURE_ABS | Absolute Evap system Vapor Pressure | Unit.kilopascal |
+| 54 | EVAP_VAPOR_PRESSURE_ALT | Evap system vapor pressure | Unit.pascal |
+| 55 | SHORT_O2_TRIM_B1 | Short term secondary O2 trim - Bank 1 | Unit.percent |
+| 56 | LONG_O2_TRIM_B1 | Long term secondary O2 trim - Bank 1 | Unit.percent |
+| 57 | SHORT_O2_TRIM_B2 | Short term secondary O2 trim - Bank 2 | Unit.percent |
+| 58 | LONG_O2_TRIM_B2 | Long term secondary O2 trim - Bank 2 | Unit.percent |
+| 59 | FUEL_RAIL_PRESSURE_ABS | Fuel rail pressure (absolute) | Unit.kilopascal |
+| 5A | RELATIVE_ACCEL_POS | Relative accelerator pedal position | Unit.percent |
+| 5B | HYBRID_BATTERY_REMAINING | Hybrid battery pack remaining life | Unit.percent |
+| 5C | OIL_TEMP | Engine oil temperature | Unit.celsius |
+| 5D | FUEL_INJECT_TIMING | Fuel injection timing | Unit.degree |
+| 5E | FUEL_RATE | Engine fuel rate | Unit.liters_per_hour |
+| 5F | *unsupported* | *unsupported* | |
+
+
+
+# Mode 02
+
+Mode 02 commands are the same as mode 01, but are metrics from when the last DTC occurred (the freeze frame). To access them by name, simple prepend `DTC_` to the Mode 01 command name.
+
+```python
+import obd
+
+obd.commands.RPM # the Mode 01 command
+# vs.
+obd.commands.DTC_RPM # the Mode 02 command
+```
+
+
+
+# Mode 03
+
+Mode 03 contains a single command `GET_DTC` which requests all diagnostic trouble codes from the vehicle. The response will contain the codes themselves, as well as a description (if python-OBD has one). See the [DTC Responses](Responses.md#diagnostic-trouble-codes-dtcs) section for more details.
+
+|PID | Name | Description | Response Value |
+|-----|---------|-----------------------------------------|-----------------------|
+| N/A | GET_DTC | Get Diagnostic Trouble Codes | [special](Responses.md#diagnostic-trouble-codes-dtcs) |
+
+
+
+
+# Mode 04
+
+|PID | Name | Description | Response Value |
+|-----|-----------|-----------------------------------------|-----------------------|
+| N/A | CLEAR_DTC | Clear DTCs and Freeze data | N/A |
+
+
+
+# Mode 06
+
+*WARNING: mode 06 is experimental. While it passes software tests, it has not been tested on a real vehicle. Any debug output for this mode would be greatly appreciated.*
+
+Mode 06 commands are used to monitor various test results from the vehicle. All commands in this mode return the same datatype, as described in the [Monitor Response](Responses.md#monitors-mode-06-responses) section. Currently, mode 06 commands are only implemented for CAN protocols (ISO 15765-4).
+
+|PID | Name | Description | Response Value |
+|-------|-----------------------------|--------------------------------------------|-----------------------|
+| 00 | MIDS_A | Supported MIDs [01-20] | bitarray |
+| 01 | MONITOR_O2_B1S1 | O2 Sensor Monitor Bank 1 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 02 | MONITOR_O2_B1S2 | O2 Sensor Monitor Bank 1 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 03 | MONITOR_O2_B1S3 | O2 Sensor Monitor Bank 1 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 04 | MONITOR_O2_B1S4 | O2 Sensor Monitor Bank 1 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 05 | MONITOR_O2_B2S1 | O2 Sensor Monitor Bank 2 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 06 | MONITOR_O2_B2S2 | O2 Sensor Monitor Bank 2 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 07 | MONITOR_O2_B2S3 | O2 Sensor Monitor Bank 2 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 08 | MONITOR_O2_B2S4 | O2 Sensor Monitor Bank 2 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 09 | MONITOR_O2_B3S1 | O2 Sensor Monitor Bank 3 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 0A | MONITOR_O2_B3S2 | O2 Sensor Monitor Bank 3 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 0B | MONITOR_O2_B3S3 | O2 Sensor Monitor Bank 3 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 0C | MONITOR_O2_B3S4 | O2 Sensor Monitor Bank 3 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 0D | MONITOR_O2_B4S1 | O2 Sensor Monitor Bank 4 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 0E | MONITOR_O2_B4S2 | O2 Sensor Monitor Bank 4 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 0F | MONITOR_O2_B4S3 | O2 Sensor Monitor Bank 4 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 10 | MONITOR_O2_B4S4 | O2 Sensor Monitor Bank 4 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) |
+| *gap* | | |
+| 20 | MIDS_B | Supported MIDs [21-40] | bitarray |
+| 21 | MONITOR_CATALYST_B1 | Catalyst Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 22 | MONITOR_CATALYST_B2 | Catalyst Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 23 | MONITOR_CATALYST_B3 | Catalyst Monitor Bank 3 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 24 | MONITOR_CATALYST_B4 | Catalyst Monitor Bank 4 | [monitor](Responses.md#monitors-mode-06-responses) |
+| *gap* | | |
+| 31 | MONITOR_EGR_B1 | EGR Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 32 | MONITOR_EGR_B2 | EGR Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 33 | MONITOR_EGR_B3 | EGR Monitor Bank 3 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 34 | MONITOR_EGR_B4 | EGR Monitor Bank 4 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 35 | MONITOR_VVT_B1 | VVT Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 36 | MONITOR_VVT_B2 | VVT Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 37 | MONITOR_VVT_B3 | VVT Monitor Bank 3 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 38 | MONITOR_VVT_B4 | VVT Monitor Bank 4 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 39 | MONITOR_EVAP_150 | EVAP Monitor (Cap Off / 0.150\") | [monitor](Responses.md#monitors-mode-06-responses) |
+| 3A | MONITOR_EVAP_090 | EVAP Monitor (0.090\") | [monitor](Responses.md#monitors-mode-06-responses) |
+| 3B | MONITOR_EVAP_040 | EVAP Monitor (0.040\") | [monitor](Responses.md#monitors-mode-06-responses) |
+| 3C | MONITOR_EVAP_020 | EVAP Monitor (0.020\") | [monitor](Responses.md#monitors-mode-06-responses) |
+| 3D | MONITOR_PURGE_FLOW | Purge Flow Monitor | [monitor](Responses.md#monitors-mode-06-responses) |
+| *gap* | | |
+| 40 | MIDS_C | Supported MIDs [41-60] | bitarray |
+| 41 | MONITOR_O2_HEATER_B1S1 | O2 Sensor Heater Monitor Bank 1 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 42 | MONITOR_O2_HEATER_B1S2 | O2 Sensor Heater Monitor Bank 1 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 43 | MONITOR_O2_HEATER_B1S3 | O2 Sensor Heater Monitor Bank 1 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 44 | MONITOR_O2_HEATER_B1S4 | O2 Sensor Heater Monitor Bank 1 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 45 | MONITOR_O2_HEATER_B2S1 | O2 Sensor Heater Monitor Bank 2 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 46 | MONITOR_O2_HEATER_B2S2 | O2 Sensor Heater Monitor Bank 2 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 47 | MONITOR_O2_HEATER_B2S3 | O2 Sensor Heater Monitor Bank 2 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 48 | MONITOR_O2_HEATER_B2S4 | O2 Sensor Heater Monitor Bank 2 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 49 | MONITOR_O2_HEATER_B3S1 | O2 Sensor Heater Monitor Bank 3 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 4A | MONITOR_O2_HEATER_B3S2 | O2 Sensor Heater Monitor Bank 3 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 4B | MONITOR_O2_HEATER_B3S3 | O2 Sensor Heater Monitor Bank 3 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 4C | MONITOR_O2_HEATER_B3S4 | O2 Sensor Heater Monitor Bank 3 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 4D | MONITOR_O2_HEATER_B4S1 | O2 Sensor Heater Monitor Bank 4 - Sensor 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 4E | MONITOR_O2_HEATER_B4S2 | O2 Sensor Heater Monitor Bank 4 - Sensor 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 4F | MONITOR_O2_HEATER_B4S3 | O2 Sensor Heater Monitor Bank 4 - Sensor 3 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 50 | MONITOR_O2_HEATER_B4S4 | O2 Sensor Heater Monitor Bank 4 - Sensor 4 | [monitor](Responses.md#monitors-mode-06-responses) |
+| *gap* | | |
+| 60 | MIDS_D | Supported MIDs [61-80] | bitarray |
+| 61 | MONITOR_HEATED_CATALYST_B1 | Heated Catalyst Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 62 | MONITOR_HEATED_CATALYST_B2 | Heated Catalyst Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 63 | MONITOR_HEATED_CATALYST_B3 | Heated Catalyst Monitor Bank 3 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 64 | MONITOR_HEATED_CATALYST_B4 | Heated Catalyst Monitor Bank 4 | [monitor](Responses.md#monitors-mode-06-responses) |
+| *gap* | | |
+| 71 | MONITOR_SECONDARY_AIR_1 | Secondary Air Monitor 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 72 | MONITOR_SECONDARY_AIR_2 | Secondary Air Monitor 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 73 | MONITOR_SECONDARY_AIR_3 | Secondary Air Monitor 3 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 74 | MONITOR_SECONDARY_AIR_4 | Secondary Air Monitor 4 | [monitor](Responses.md#monitors-mode-06-responses) |
+| *gap* | | |
+| 80 | MIDS_E | Supported MIDs [81-A0] | bitarray |
+| 81 | MONITOR_FUEL_SYSTEM_B1 | Fuel System Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 82 | MONITOR_FUEL_SYSTEM_B2 | Fuel System Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 83 | MONITOR_FUEL_SYSTEM_B3 | Fuel System Monitor Bank 3 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 84 | MONITOR_FUEL_SYSTEM_B4 | Fuel System Monitor Bank 4 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 85 | MONITOR_BOOST_PRESSURE_B1 | Boost Pressure Control Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 86 | MONITOR_BOOST_PRESSURE_B2 | Boost Pressure Control Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| *gap* | | |
+| 90 | MONITOR_NOX_ABSORBER_B1 | NOx Absorber Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 91 | MONITOR_NOX_ABSORBER_B2 | NOx Absorber Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+| *gap* | | |
+| 98 | MONITOR_NOX_CATALYST_B1 | NOx Catalyst Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| 99 | MONITOR_NOX_CATALYST_B2 | NOx Catalyst Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+| *gap* | | |
+| A0 | MIDS_F | Supported MIDs [A1-C0] | bitarray |
+| A1 | MONITOR_MISFIRE_GENERAL | Misfire Monitor General Data | [monitor](Responses.md#monitors-mode-06-responses) |
+| A2 | MONITOR_MISFIRE_CYLINDER_1 | Misfire Cylinder 1 Data | [monitor](Responses.md#monitors-mode-06-responses) |
+| A3 | MONITOR_MISFIRE_CYLINDER_2 | Misfire Cylinder 2 Data | [monitor](Responses.md#monitors-mode-06-responses) |
+| A4 | MONITOR_MISFIRE_CYLINDER_3 | Misfire Cylinder 3 Data | [monitor](Responses.md#monitors-mode-06-responses) |
+| A5 | MONITOR_MISFIRE_CYLINDER_4 | Misfire Cylinder 4 Data | [monitor](Responses.md#monitors-mode-06-responses) |
+| A6 | MONITOR_MISFIRE_CYLINDER_5 | Misfire Cylinder 5 Data | [monitor](Responses.md#monitors-mode-06-responses) |
+| A7 | MONITOR_MISFIRE_CYLINDER_6 | Misfire Cylinder 6 Data | [monitor](Responses.md#monitors-mode-06-responses) |
+| A8 | MONITOR_MISFIRE_CYLINDER_7 | Misfire Cylinder 7 Data | [monitor](Responses.md#monitors-mode-06-responses) |
+| A9 | MONITOR_MISFIRE_CYLINDER_8 | Misfire Cylinder 8 Data | [monitor](Responses.md#monitors-mode-06-responses) |
+| AA | MONITOR_MISFIRE_CYLINDER_9 | Misfire Cylinder 9 Data | [monitor](Responses.md#monitors-mode-06-responses) |
+| AB | MONITOR_MISFIRE_CYLINDER_10 | Misfire Cylinder 10 Data | [monitor](Responses.md#monitors-mode-06-responses) |
+| AC | MONITOR_MISFIRE_CYLINDER_11 | Misfire Cylinder 11 Data | [monitor](Responses.md#monitors-mode-06-responses) |
+| AD | MONITOR_MISFIRE_CYLINDER_12 | Misfire Cylinder 12 Data | [monitor](Responses.md#monitors-mode-06-responses) |
+| *gap* | | |
+| B0 | MONITOR_PM_FILTER_B1 | PM Filter Monitor Bank 1 | [monitor](Responses.md#monitors-mode-06-responses) |
+| B1 | MONITOR_PM_FILTER_B2 | PM Filter Monitor Bank 2 | [monitor](Responses.md#monitors-mode-06-responses) |
+
+
+
+# Mode 07
+
+The return value will be encoded in the same structure as the Mode 03 `GET_DTC` command.
+
+|PID | Name | Description | Response Value |
+|-----|-----------------|----------------------------------------------|-----------------------|
+| N/A | GET_CURRENT_DTC | Get DTCs from the current/last driving cycle | [special](Responses.md#diagnostic-trouble-codes-dtcs) |
+
+
diff --git a/docs/Commands.md b/docs/Commands.md
deleted file mode 100644
index ab670aee..00000000
--- a/docs/Commands.md
+++ /dev/null
@@ -1,233 +0,0 @@
-
-# Lookup
-
-`OBDCommand`s are objects used to query information from the vehicle. They contain all of the information neccessary to perform the query, and decode the cars response. Python-OBD has built in tables for the most common commands. They can be looked up by name, or by mode & PID.
-
-```python
-import obd
-
-c = obd.commands.RPM
-
-# OR
-
-c = obd.commands['RPM']
-
-# OR
-
-c = obd.commands[1][12] # mode 1, PID 12 (RPM)
-```
-
-The `commands` table also has a few helper methods for determining if a particular name or PID is present.
-
----
-
-### has_command(command)
-
-Checks the internal command tables for the existance of the given `OBDCommand` object. Commands are compared by mode and PID value.
-
-```python
-import obd
-obd.commands.has_command(obd.commands.RPM) # True
-```
-
----
-
-### has_name(name)
-
-Checks the internal command tables for a command with the given name. This is also the function of the `in` operator.
-
-```python
-import obd
-
-obd.commands.has_name('RPM') # True
-
-# OR
-
-'RPM' in obd.commands # True
-```
-
----
-
-### has_pid(mode, pid)
-
-Checks the internal command tables for a command with the given mode and PID.
-
-```python
-import obd
-obd.commands.has_pid(1, 12) # True
-```
-
----
-
-
-
-# OBD-II adapter (ELM327 commands)
-
-|PID | Name | Description |
-|-----|-------------|-----------------------------------------|
-| N/A | ELM_VERSION | OBD-II adapter version string |
-| N/A | ELM_VOLTAGE | Voltage detected by OBD-II adapter |
-
-
-
-# Mode 01
-
-|PID | Name | Description |
-|----|---------------------------|-----------------------------------------|
-| 00 | PIDS_A | Supported PIDs [01-20] |
-| 01 | STATUS | Status since DTCs cleared |
-| 02 | *unsupported* | *unsupported* |
-| 03 | FUEL_STATUS | Fuel System Status |
-| 04 | ENGINE_LOAD | Calculated Engine Load |
-| 05 | COOLANT_TEMP | Engine Coolant Temperature |
-| 06 | SHORT_FUEL_TRIM_1 | Short Term Fuel Trim - Bank 1 |
-| 07 | LONG_FUEL_TRIM_1 | Long Term Fuel Trim - Bank 1 |
-| 08 | SHORT_FUEL_TRIM_2 | Short Term Fuel Trim - Bank 2 |
-| 09 | LONG_FUEL_TRIM_2 | Long Term Fuel Trim - Bank 2 |
-| 0A | FUEL_PRESSURE | Fuel Pressure |
-| 0B | INTAKE_PRESSURE | Intake Manifold Pressure |
-| 0C | RPM | Engine RPM |
-| 0D | SPEED | Vehicle Speed |
-| 0E | TIMING_ADVANCE | Timing Advance |
-| 0F | INTAKE_TEMP | Intake Air Temp |
-| 10 | MAF | Air Flow Rate (MAF) |
-| 11 | THROTTLE_POS | Throttle Position |
-| 12 | AIR_STATUS | Secondary Air Status |
-| 13 | *unsupported* | *unsupported* |
-| 14 | O2_B1S1 | O2: Bank 1 - Sensor 1 Voltage |
-| 15 | O2_B1S2 | O2: Bank 1 - Sensor 2 Voltage |
-| 16 | O2_B1S3 | O2: Bank 1 - Sensor 3 Voltage |
-| 17 | O2_B1S4 | O2: Bank 1 - Sensor 4 Voltage |
-| 18 | O2_B2S1 | O2: Bank 2 - Sensor 1 Voltage |
-| 19 | O2_B2S2 | O2: Bank 2 - Sensor 2 Voltage |
-| 1A | O2_B2S3 | O2: Bank 2 - Sensor 3 Voltage |
-| 1B | O2_B2S4 | O2: Bank 2 - Sensor 4 Voltage |
-| 1C | OBD_COMPLIANCE | OBD Standards Compliance |
-| 1D | *unsupported* | *unsupported* |
-| 1E | *unsupported* | *unsupported* |
-| 1F | RUN_TIME | Engine Run Time |
-| 20 | PIDS_B | Supported PIDs [21-40] |
-| 21 | DISTANCE_W_MIL | Distance Traveled with MIL on |
-| 22 | FUEL_RAIL_PRESSURE_VAC | Fuel Rail Pressure (relative to vacuum) |
-| 23 | FUEL_RAIL_PRESSURE_DIRECT | Fuel Rail Pressure (direct inject) |
-| 24 | O2_S1_WR_VOLTAGE | 02 Sensor 1 WR Lambda Voltage |
-| 25 | O2_S2_WR_VOLTAGE | 02 Sensor 2 WR Lambda Voltage |
-| 26 | O2_S3_WR_VOLTAGE | 02 Sensor 3 WR Lambda Voltage |
-| 27 | O2_S4_WR_VOLTAGE | 02 Sensor 4 WR Lambda Voltage |
-| 28 | O2_S5_WR_VOLTAGE | 02 Sensor 5 WR Lambda Voltage |
-| 29 | O2_S6_WR_VOLTAGE | 02 Sensor 6 WR Lambda Voltage |
-| 2A | O2_S7_WR_VOLTAGE | 02 Sensor 7 WR Lambda Voltage |
-| 2B | O2_S8_WR_VOLTAGE | 02 Sensor 8 WR Lambda Voltage |
-| 2C | COMMANDED_EGR | Commanded EGR |
-| 2D | EGR_ERROR | EGR Error |
-| 2E | EVAPORATIVE_PURGE | Commanded Evaporative Purge |
-| 2F | FUEL_LEVEL | Fuel Level Input |
-| 30 | WARMUPS_SINCE_DTC_CLEAR | Number of warm-ups since codes cleared |
-| 31 | DISTANCE_SINCE_DTC_CLEAR | Distance traveled since codes cleared |
-| 32 | EVAP_VAPOR_PRESSURE | Evaporative system vapor pressure |
-| 33 | BAROMETRIC_PRESSURE | Barometric Pressure |
-| 34 | O2_S1_WR_CURRENT | 02 Sensor 1 WR Lambda Current |
-| 35 | O2_S2_WR_CURRENT | 02 Sensor 2 WR Lambda Current |
-| 36 | O2_S3_WR_CURRENT | 02 Sensor 3 WR Lambda Current |
-| 37 | O2_S4_WR_CURRENT | 02 Sensor 4 WR Lambda Current |
-| 38 | O2_S5_WR_CURRENT | 02 Sensor 5 WR Lambda Current |
-| 39 | O2_S6_WR_CURRENT | 02 Sensor 6 WR Lambda Current |
-| 3A | O2_S7_WR_CURRENT | 02 Sensor 7 WR Lambda Current |
-| 3B | O2_S8_WR_CURRENT | 02 Sensor 8 WR Lambda Current |
-| 3C | CATALYST_TEMP_B1S1 | Catalyst Temperature: Bank 1 - Sensor 1 |
-| 3D | CATALYST_TEMP_B2S1 | Catalyst Temperature: Bank 2 - Sensor 1 |
-| 3E | CATALYST_TEMP_B1S2 | Catalyst Temperature: Bank 1 - Sensor 2 |
-| 3F | CATALYST_TEMP_B2S2 | Catalyst Temperature: Bank 2 - Sensor 2 |
-| 40 | PIDS_C | Supported PIDs [41-60] |
-| 41 | *unsupported* | *unsupported* |
-| 42 | *unsupported* | *unsupported* |
-| 43 | *unsupported* | *unsupported* |
-| 44 | *unsupported* | *unsupported* |
-| 45 | RELATIVE_THROTTLE_POS | Relative throttle position |
-| 46 | AMBIANT_AIR_TEMP | Ambient air temperature |
-| 47 | THROTTLE_POS_B | Absolute throttle position B |
-| 48 | THROTTLE_POS_C | Absolute throttle position C |
-| 49 | ACCELERATOR_POS_D | Accelerator pedal position D |
-| 4A | ACCELERATOR_POS_E | Accelerator pedal position E |
-| 4B | ACCELERATOR_POS_F | Accelerator pedal position F |
-| 4C | THROTTLE_ACTUATOR | Commanded throttle actuator |
-| 4D | RUN_TIME_MIL | Time run with MIL on |
-| 4E | TIME_SINCE_DTC_CLEARED | Time since trouble codes cleared |
-| 4F | *unsupported* | *unsupported* |
-| 50 | MAX_MAF | Maximum value for mass air flow sensor |
-| 51 | FUEL_TYPE | Fuel Type |
-| 52 | ETHANOL_PERCENT | Ethanol Fuel Percent |
-| 53 | EVAP_VAPOR_PRESSURE_ABS | Absolute Evap system Vapor Pressure |
-| 54 | EVAP_VAPOR_PRESSURE_ALT | Evap system vapor pressure |
-| 55 | SHORT_O2_TRIM_B1 | Short term secondary O2 trim - Bank 1 |
-| 56 | LONG_O2_TRIM_B1 | Long term secondary O2 trim - Bank 1 |
-| 57 | SHORT_O2_TRIM_B2 | Short term secondary O2 trim - Bank 2 |
-| 58 | LONG_O2_TRIM_B2 | Long term secondary O2 trim - Bank 2 |
-| 59 | FUEL_RAIL_PRESSURE_ABS | Fuel rail pressure (absolute) |
-| 5A | RELATIVE_ACCEL_POS | Relative accelerator pedal position |
-| 5B | HYBRID_BATTERY_REMAINING | Hybrid battery pack remaining life |
-| 5C | OIL_TEMP | Engine oil temperature |
-| 5D | FUEL_INJECT_TIMING | Fuel injection timing |
-| 5E | FUEL_RATE | Engine fuel rate |
-| 5F | *unsupported* | *unsupported* |
-
-
-
-# Mode 02
-
-Mode 02 commands are the same as mode 01, but are metrics from when the last DTC occurred (the freeze frame). To access them by name, simple prepend `DTC_` to the Mode 01 command name.
-
-```python
-import obd
-
-obd.commands.RPM # the Mode 01 command
-# vs.
-obd.commands.DTC_RPM # the Mode 02 command
-```
-
-
-
-# Mode 03
-
-Mode 03 contains a single command `GET_DTC` which requests all diagnostic trouble codes from the vehicle's engine.
-
-|PID | Name | Description |
-|-----|---------|-----------------------------------------|
-| N/A | GET_DTC | Get Diagnostic Trouble Codes |
-
-This command requests all diagnostic trouble codes from the vehicle's engine. The `value` field of the response object will contain a list of tuples, where each tuple contains the DTC, and a string description of that DTC (if available).
-
-```python
-import obd
-connection = obd.OBD()
-r = connection.query(obd.commands.GET_DTC)
-print(r.value)
-
-'''
-example output:
-[
- ("P0030", "HO2S Heater Control Circuit"),
- ("P1367", "Unknown error code")
-]
-'''
-```
-
-
-
-# Mode 04
-
-|PID | Name | Description |
-|-----|-----------|-----------------------------------------|
-| N/A | CLEAR_DTC | Clear DTCs and Freeze data |
-
-
-
-# Mode 07
-
-The return value will be encoded in the same structure as the Mode 03 `GET_DTC` command.
-
-|PID | Name | Description |
-|-----|----------------|------------------------------|
-| N/A | GET_FREEZE_DTC | Get Freeze DTCs |
-
-
diff --git a/docs/Connections.md b/docs/Connections.md
index 36965ab6..a6dbfa94 100644
--- a/docs/Connections.md
+++ b/docs/Connections.md
@@ -12,7 +12,7 @@ connection = obd.OBD("/dev/ttyUSB0") # create connection with USB 0
# OR
-ports = obd.scan_serial() # return list of valid USB or RF ports
+ports = obd.scan_serial() # return list of valid USB or RF ports
print ports # ['/dev/ttyUSB0', '/dev/ttyUSB1']
connection = obd.OBD(ports[0]) # connect to the first port in the list
```
@@ -20,13 +20,13 @@ connection = obd.OBD(ports[0]) # connect to the first port in the list
-### OBD(portstr=None, baudrate=38400, protocol=None, fast=True):
+### OBD(portstr=None, baudrate=None, protocol=None, fast=True):
`portstr`: The UNIX device file or Windows COM Port for your adapter. The default value (`None`) will auto select a port.
-`baudrate`: The baudrate at which to set the serial connection. This can vary from adapter to adapter. Typical values are: 9600, 38400, 19200, 57600, 115200
+`baudrate`: The baudrate at which to set the serial connection. This can vary from adapter to adapter. Typical values are: 9600, 38400, 19200, 57600, 115200. The default value (`None`) will auto select a baudrate.
-`protocol`: Forces python-OBD to use the given protocol when communicating with the adapter. See `protocol_id()` for possible values. The default value (`None`) will auto select a protocol.
+`protocol`: Forces python-OBD to use the given protocol when communicating with the adapter. See [protocol_id()](Connections.md/#protocol_id) for possible values. The default value (`None`) will auto select a protocol.
`fast`: Allows commands to be optimized before being sent to the car. Python-OBD currently makes two such optimizations:
@@ -41,7 +41,7 @@ Disabling fast mode will guarantee that python-OBD outputs the unaltered command
### query(command, force=False)
-Sends an `OBDCommand` to the car, and returns a `OBDResponse` object. This function will block until a response is received from the car. This function will also check whether the given command is supported by your car. If a command is not marked as supported, it will not be sent to the car, and an empty `Response` will be returned. To force an unsupported command to be sent, there is an optional `force` parameter for your convenience.
+Sends an `OBDCommand` to the car, and returns an `OBDResponse` object. This function will block until a response is received from the car. This function will also check whether the given command is supported by your car. If a command is not marked as supported, it will not be sent, and an empty `OBDResponse` will be returned. To force an unsupported command to be sent, there is an optional `force` parameter for your convenience.
*For non-blocking querying, see [Async Querying](Async Connections.md)*
@@ -87,13 +87,7 @@ connection.status() == OBDStatus.CAR_CONNECTED
### port_name()
-Returns the string name for the currently connected port (`"/dev/ttyUSB0"`). If no connection was made, this function returns `"Not connected to any port"`.
-
----
-
-### get_port_name()
-
-**Deprecated:** use `port_name()` instead
+Returns the string name for the currently connected port (`"/dev/ttyUSB0"`). If no connection was made, this function returns an empty string.
---
@@ -148,8 +142,17 @@ Closes the connection.
### supported_commands
-Property containing a list of commands that are supported by the car.
+Property containing a `set` of commands that are supported by the car.
+
+If you wish to manually mark a command as supported (prevents having to use `query(force=True)`), add the command to this set. This is not necessary when using python-OBD's builtin commands, but is useful if you create [custom commands](Custom Commands.md).
+```python
+import obd
+connection = obd.OBD()
+
+# manually mark the given command as supported
+connection.supported_commands.add()
+```
---
diff --git a/docs/Custom Commands.md b/docs/Custom Commands.md
index 150cb412..cef34172 100644
--- a/docs/Custom Commands.md
+++ b/docs/Custom Commands.md
@@ -5,8 +5,8 @@ If the command you need is not in python-OBDs tables, you can create a new `OBDC
|----------------------|----------|----------------------------------------------------------------------------|
| name | string | (human readability only) |
| desc | string | (human readability only) |
-| command | string | OBD command in hex (typically mode + PID |
-| bytes | int | Number of bytes expected in response |
+| command | bytes | OBD command in hex (typically mode + PID |
+| bytes | int | Number of bytes expected in response (zero means unknown) |
| decoder | callable | Function used for decoding messages from the OBD adapter |
| ecu (optional) | ECU | ID of the ECU this command should listen to (`ECU.ALL` by default) |
| fast (optional) | bool | Allows python-OBD to alter this command for efficieny (`False` by default) |
@@ -21,13 +21,14 @@ from obd.protocols import ECU
from obd.utils import bytes_to_int
def rpm(messages):
+ """ decoder for RPM messages """
d = messages[0].data
v = bytes_to_int(d) / 4.0 # helper function for converting byte arrays to ints
- return (v, Unit.RPM)
+ return v * Unit.RPM # construct a Pint Quantity
c = OBDCommand("RPM", \ # name
"Engine RPM", \ # description
- "010C", \ # command
+ b"010C", \ # command
2, \ # number of return bytes to expect
rpm, \ # decoding function
ECU.ENGINE, \ # (optional) ECU filter
@@ -37,16 +38,14 @@ c = OBDCommand("RPM", \ # name
By default, custom commands will be treated as "unsupported by the vehicle". There are two ways to handle this:
```python
-# use the `force` parameter when querying
o = obd.OBD()
+
+# use the `force` parameter when querying
o.query(c, force=True)
-```
-or
+# OR
-```python
# add your command to the set of supported commands
-o = obd.OBD()
o.supported_commands.add(c)
o.query(c)
```
@@ -64,10 +63,10 @@ The `decoder` argument is a function of following form.
```python
def ():
...
- return (, )
+ return
```
-Decoders are given a list of `Message` objects as an argument. If your decoder is called, this list is garaunteed to have at least one message object. Each `Message` object has a `data` property, which holds a parsed byte array, and is also garauteed to have the number of bytes specified by the command.
+The return value of your decoder will be loaded into the `OBDResponse.value` field. Decoders are given a list of `Message` objects as an argument. If your decoder is called, this list is garaunteed to have at least one message object. Each `Message` object has a `data` property, which holds a parsed bytearray, and is also garauteed to have the number of bytes specified by the command.
*NOTE: If you are transitioning from an older version of Python-OBD (where decoders were given raw hex strings as arguments), you can use the `Message.hex()` function as a patch.*
@@ -75,7 +74,7 @@ Decoders are given a list of `Message` objects as an argument. If your decoder i
def (messages):
_hex = messages[0].hex()
...
- return (, )
+ return
```
*You can also access the original string sent by the adapter using the `Message.raw()` function.*
diff --git a/docs/Debug.md b/docs/Debug.md
index 0cacdb7c..0f929826 100644
--- a/docs/Debug.md
+++ b/docs/Debug.md
@@ -1,16 +1,17 @@
-python-OBD also contains a debug object that receives status messages and errors. Console printing is disabled by default, but can be enabled manually. A custom debug handler can also be set.
+python-OBD uses python's builtin logging system. By default, it is setup to send output to `stderr` with a level of WARNING. The module's logger can be accessed via the `logger` variable at the root of the module. For instance, to enable console printing of all debug messages, use the following snippet:
```python
import obd
-obd.debug.console = True
+obd.logger.setLevel(obd.logging.DEBUG) # enables all debug information
+```
-# AND / OR
+Or, to silence all logging output from python-OBD:
-def log(msg):
- print msg
+```python
+import obd
-obd.debug.handler = log
+obd.logger.removeHandler(obd.console_handler)
```
---
diff --git a/docs/Responses.md b/docs/Responses.md
index fe72b3b6..30b992e6 100644
--- a/docs/Responses.md
+++ b/docs/Responses.md
@@ -3,12 +3,10 @@ The `query()` function returns `OBDResponse` objects. These objects have the fol
| Property | Description |
|----------|------------------------------------------------------------------------|
| value | The decoded value from the car |
-| unit | The units of the decoded value |
-| command | The `OBDCommand` object that triggered this response |
+| command | The `OBDCommand` object that triggered this response |
| message | The internal `Message` object containing the raw response from the car |
| time | Timestamp of response (as given by [`time.time()`](https://docs.python.org/2/library/time.html#time.time)) |
-The `value` property typically contains numeric values, but can also hold complex structures (depending upon the command that was sent).
---
@@ -27,36 +25,229 @@ if not r.is_null():
---
-# Units
+# Pint Values
-Unit values can be found in the `Unit` class (enum).
+The `value` property typically contains a [Pint](http://pint.readthedocs.io/en/latest/) `Quantity` object, but can also hold complex structures (depending on the request). Pint quantities combine a value and unit into a single class, and are used to represent physical values such as "4 seconds", and "88 mph". This allows for consistency when doing math and unit conversions. Pint maintains a registry of units, which is exposed in python-OBD as `obd.Unit`.
+
+Below are common operations that can be done with Pint units and quantities. For more information, check out the [Pint Documentation](http://pint.readthedocs.io/en/latest/).
+
+*NOTE: for backwards compatibility with previous versions of python-OBD, use `response.value.magnitude` in place of `response.value`*
+
+```python
+import obd
+
+>>> response.value
+
+
+# get the raw python datatype
+>>> response.value.magnitude
+100
+
+# converts quantities to strings
+>>> str(response.value)
+'100 kph'
+
+# convert strings to quantities
+>>> obd.Unit("100 kph")
+
+
+# handles conversions nicely
+>>> response.value.to('mph')
+
+
+# scaler math
+>>> response.value / 2
+
+
+# non-scaler math requires you to specify units yourself
+>>> response.value + (20 * obd.Unit.kph)
+
+
+# non-scaler math with different units
+# handles unit conversions transparently
+>>> response.value + (20 * obd.Unit.mph)
+
+```
+
+---
+
+# Status
+
+The status command returns information about the Malfunction Indicator Light (check-engine light), the number of trouble codes being thrown, and the type of engine.
+
+```python
+response.value.MIL # boolean for whether the check-engine is lit
+response.value.DTC_count # number (int) of DTCs being thrown
+response.value.ignition_type # "spark" or "compression"
+```
+
+The status command also provides information regarding the availability and status of various system tests. These are exposed as `StatusTest` objects, loaded into named properties. Each test object has boolean flags for its availability and completion.
+
+```python
+response.value.MISFIRE_MONITORING.available # boolean for test availability
+response.value.MISFIRE_MONITORING.complete # boolean for test completion
+```
+
+Here are all of the tests names that python-OBD reports:
+
+| Tests |
+|-----------------------------------|
+| MISFIRE_MONITORING |
+| FUEL_SYSTEM_MONITORING |
+| COMPONENT_MONITORING |
+| CATALYST_MONITORING |
+| HEATED_CATALYST_MONITORING |
+| EVAPORATIVE_SYSTEM_MONITORING |
+| SECONDARY_AIR_SYSTEM_MONITORING |
+| OXYGEN_SENSOR_MONITORING |
+| OXYGEN_SENSOR_HEATER_MONITORING |
+| EGR_VVT_SYSTEM_MONITORING |
+| NMHC_CATALYST_MONITORING |
+| NOX_SCR_AFTERTREATMENT_MONITORING |
+| BOOST_PRESSURE_MONITORING |
+| EXHAUST_GAS_SENSOR_MONITORING |
+| PM_FILTER_MONITORING |
+
+
+---
+
+# Diagnostic Trouble Codes (DTCs)
+
+Each DTC is represented by a tuple containing the DTC code, and a description (if python-OBD has one). For commands that return multiple DTCs, a list is used.
+
+```python
+# obd.commands.GET_DTC
+response.value = [
+ ("P0104", "Mass or Volume Air Flow Circuit Intermittent"),
+ ("B0003", ""), # unknown error code, it's probably vehicle-specific
+ ("C0123", "")
+]
+
+# obd.commands.FREEZE_DTC
+response.value = ("P0104", "Mass or Volume Air Flow Circuit Intermittent")
+```
+
+---
+
+# Fuel Status
+
+The fuel status is a tuple of two strings, telling the status of the first and second fuel systems. Most cars only have one system, so the second element will likely be an empty string. The possible fuel statuses are:
+
+| Fuel Status |
+| ----------------------------------------------------------------------------------------------|
+| `""` |
+| `"Open loop due to insufficient engine temperature"` |
+| `"Closed loop, using oxygen sensor feedback to determine fuel mix"` |
+| `"Open loop due to engine load OR fuel cut due to deceleration"` |
+| `"Open loop due to system failure"` |
+| `"Closed loop, using at least one oxygen sensor but there is a fault in the feedback system"` |
+
+---
+
+# Air Status
+
+The air status will be one of these strings:
+
+| Air Status |
+| ---------------------------------------|
+| `"Upstream"` |
+| `"Downstream of catalytic converter"` |
+| `"From the outside atmosphere or off"` |
+| `"Pump commanded on for diagnostics"` |
+
+---
+
+# Oxygen Sensors Present
+
+Returns a 2D structure of tuples (representing bank and sensor number), that holds boolean values for sensor presence.
```python
-from obd.utils import Unit
+# obd.commands.O2_SENSORS
+response.value = (
+ (), # bank 0 is invalid, this is merely for correct indexing
+ (True, True, True, False), # bank 1
+ (False, False, False, False) # bank 2
+)
+
+# obd.commands.O2_SENSORS_ALT
+response.value = (
+ (), # bank 0 is invalid, this is merely for correct indexing
+ (True, True), # bank 1
+ (True, False), # bank 2
+ (False, False), # bank 3
+ (False, False) # bank 4
+)
+
+# example usage:
+response.value[1][2] == True # Bank 1, Sensor 2 is present
```
+---
+
+# Monitors (Mode 06 Responses)
-| Name | Value |
-|-------------|--------------------|
-| NONE | None |
-| RATIO | "Ratio" |
-| COUNT | "Count" |
-| PERCENT | "%" |
-| RPM | "RPM" |
-| VOLT | "Volt" |
-| F | "F" |
-| C | "C" |
-| SEC | "Second" |
-| MIN | "Minute" |
-| PA | "Pa" |
-| KPA | "kPa" |
-| PSI | "psi" |
-| KPH | "kph" |
-| MPH | "mph" |
-| DEGREES | "Degrees" |
-| GPS | "Grams per Second" |
-| MA | "mA" |
-| KM | "km" |
-| LPH | "Liters per Hour" |
+All mode 06 commands return `Monitor` objects holding various test results for the requested sensor. A single monitor response can hold multiple tests, in the form of `MonitorTest` objects. The OBD standard defines some tests, but vehicles can always implement custom tests beyond the standard. Here are the standard Test IDs (TIDs) that python-OBD will recognize:
+
+| TID | Name | Description |
+|-----|--------------------------|----------------------------------------------------|
+| 01 | RTL_THRESHOLD_VOLTAGE | Rich to lean sensor threshold voltage |
+| 02 | LTR_THRESHOLD_VOLTAGE | Lean to rich sensor threshold voltage |
+| 03 | LOW_VOLTAGE_SWITCH_TIME | Low sensor voltage for switch time calculation |
+| 04 | HIGH_VOLTAGE_SWITCH_TIME | High sensor voltage for switch time calculation |
+| 05 | RTL_SWITCH_TIME | Rich to lean sensor switch time |
+| 06 | LTR_SWITCH_TIME | Lean to rich sensor switch time |
+| 07 | MIN_VOLTAGE | Minimum sensor voltage for test cycle |
+| 08 | MAX_VOLTAGE | Maximum sensor voltage for test cycle |
+| 09 | TRANSITION_TIME | Time between sensor transitions |
+| 0A | SENSOR_PERIOD | Sensor period |
+| 0B | MISFIRE_AVERAGE | Average misfire counts for last ten driving cycles |
+| 0C | MISFIRE_COUNT | Misfire counts for last/current driving cycles |
+
+Test results can be accessed by property name or TID (same as the `obd.commands` tables). All of the standard tests above will be present, though some may be null. Use the `MonitorTest.is_null()` function to determine if a test is null.
+
+```python
+response.value.MISFIRE_COUNT
+
+# OR
+
+response.value["MISFIRE_COUNT"]
+
+# OR
+
+response.value[0x0C] # TID for MISFIRE_COUNT
+```
+
+All `MonitorTest` objects have the following properties: (for null tests, these are set to `None`)
+
+```python
+result = response.value.MISFIRE_COUNT
+
+result.tid # integer Test ID for this test
+result.name # test name
+result.desc # test description
+result.value # value of the test (will be a Pint value, or in rare cases, a boolean)
+result.min # maximum acceptable value
+result.max # minimum acceptable value
+result.passed # boolean marking the test as passing
+```
+
+Here is an example of looking up live misfire counts for the engine's second cylinder:
+
+```python
+import obd
+
+connection = obd.OBD()
+
+response = connection.query(obd.commands.MONITOR_MISFIRE_CYLINDER_2)
+
+# in the test results, lookup the result for MISFIRE_COUNT
+result = response.value.MISFIRE_COUNT
+
+# check that we got data for this test
+if not result.is_null():
+ print(result.value) # will be a Pint value
+else:
+ print("Misfire count wasn't reported")
+```
---
diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md
index 716d3f3d..f2b9baca 100644
--- a/docs/Troubleshooting.md
+++ b/docs/Troubleshooting.md
@@ -4,7 +4,7 @@
If python-OBD is not working properly, the first thing you should do is enable debug output. Add the following line before your connection code to print all of the debug information to your console:
```python
-obd.debug.console = True
+obd.logger.setLevel(obd.logging.DEBUG)
```
Here are some common logs from python-OBD, and their meanings:
diff --git a/docs/assets/extra.js b/docs/assets/extra.js
new file mode 100644
index 00000000..5c927480
--- /dev/null
+++ b/docs/assets/extra.js
@@ -0,0 +1,48 @@
+
+$(document).ready(function () {
+ fixSearch();
+});
+
+/*
+ * RTD messes up MkDocs' search feature by tinkering with the search box defined in the theme, see
+ * https://github.com/rtfd/readthedocs.org/issues/1088. This function sets up a DOM4 MutationObserver
+ * to react to changes to the search form (triggered by RTD on doc ready). It then reverts everything
+ * the RTD JS code modified.
+ */
+
+function fixSearch()
+{
+ var target = document.getElementById('rtd-search-form');
+ var config = {attributes: true, childList: true};
+
+ var observer = new MutationObserver(function(mutations) {
+ // if it isn't disconnected it'll loop infinitely because the observed element is modified
+ observer.disconnect();
+ var form = $('#rtd-search-form');
+ form.empty();
+ form.attr('action', 'https://' + window.location.hostname + '/en/' + determineSelectedBranch() + '/search.html');
+ $('').attr({
+ type: "text",
+ name: "q",
+ placeholder: "Search docs"
+ }).appendTo(form);
+ });
+
+ // don't run this outside RTD hosting
+ if (window.location.origin.indexOf('readthedocs') > -1)
+ {
+ observer.observe(target, config);
+ }
+}
+
+function determineSelectedBranch()
+{
+ var branch = 'dev', path = window.location.pathname;
+ if (window.location.origin.indexOf('readthedocs') > -1)
+ {
+ // path is like /en///build/ -> extract 'lang'
+ // split[0] is an '' because the path starts with the separator
+ branch = path.split('/')[2];
+ }
+ return branch;
+}
diff --git a/docs/index.md b/docs/index.md
index 05282dce..7688edea 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -2,6 +2,8 @@
Python-OBD is a library for handling data from a car's [**O**n-**B**oard **D**iagnostics port](https://en.wikipedia.org/wiki/On-board_diagnostics) (OBD-II). It can stream real time sensor data, perform diagnostics (such as reading check-engine codes), and is fit for the Raspberry Pi. This library is designed to work with standard [ELM327 OBD-II adapters](http://www.amazon.com/s/ref=nb_sb_noss?field-keywords=elm327).
+*NOTE: Python-OBD is below 1.0.0, meaning the API may change between minor versions. Consult the [GitHub release page](https://github.com/brendan-w/python-OBD/releases) for changelogs before updating.*
+
# Installation
@@ -27,15 +29,33 @@ import obd
connection = obd.OBD() # auto-connects to USB or RF port
-cmd = obd.commands.RPM # select an OBD command (sensor)
+cmd = obd.commands.SPEED # select an OBD command (sensor)
response = connection.query(cmd) # send the command, and parse the response
-print(response.value)
-print(response.unit)
+print(response.value) # returns unit-bearing values thanks to Pint
+print(response.value.to("mph")) # user-friendly unit conversions
```
-OBD connections operate in a request-reply fashion. To retrieve data from the car, you must send commands that query for the data you want (e.g. RPM, Vehicle speed, etc). In python-OBD, this is done with the `query()` function. The commands themselves are represented as objects, and can be looked up by name or value in `obd.commands`. The `query()` function will return a response object with parsed data in its `value` and `unit` properties.
+OBD connections operate in a request-reply fashion. To retrieve data from the car, you must send commands that query for the data you want (e.g. RPM, Vehicle speed, etc). In python-OBD, this is done with the `query()` function. The commands themselves are represented as objects, and can be looked up by name or value in `obd.commands`. The `query()` function will return a response object with parsed data in its `value` property.
+
+
+
+# Module Layout
+
+```python
+import obd
+
+obd.OBD # main OBD connection class
+obd.Async # asynchronous OBD connection class
+obd.commands # command tables
+obd.Unit # unit tables (a Pint UnitRegistry)
+obd.OBDStatus # enum for connection status
+obd.scan_serial # util function for manually scanning for OBD adapters
+obd.OBDCommand # class for making your own OBD Commands
+obd.ECU # enum for marking which ECU a command should listen to
+obd.logger # the OBD module's root logger (for debug)
+```
diff --git a/mkdocs.yml b/mkdocs.yml
index d0800f49..bec2ac65 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,10 +1,13 @@
site_name: python-OBD
repo_url: https://github.com/brendan-w/python-OBD
repo_name: GitHub
+extra_javascript:
+- assets/extra.js
pages:
- 'Getting Started': 'index.md'
- 'OBD Connections': 'Connections.md'
-- 'Commands': 'Commands.md'
+- 'Command Lookup': 'Command Lookup.md'
+- 'Command Tables' : 'Command Tables.md'
- 'Responses': 'Responses.md'
- 'Async Connections': 'Async Connections.md'
- 'Custom Commands': 'Custom Commands.md'
diff --git a/obd/OBDCommand.py b/obd/OBDCommand.py
index 65a365e5..2a3f2e09 100644
--- a/obd/OBDCommand.py
+++ b/obd/OBDCommand.py
@@ -30,10 +30,13 @@
########################################################################
from .utils import *
-from .debug import debug
from .protocols import ECU
from .OBDResponse import OBDResponse
+import logging
+
+logger = logging.getLogger(__name__)
+
class OBDCommand():
def __init__(self,
@@ -62,24 +65,19 @@ def clone(self):
self.fast)
@property
- def mode_int(self):
+ def mode(self):
if len(self.command) >= 2:
- return unhex(self.command[:2])
+ return int(self.command[:2], 16)
else:
return 0
@property
- def pid_int(self):
+ def pid(self):
if len(self.command) > 2:
- return unhex(self.command[2:])
+ return int(self.command[2:], 16)
else:
return 0
- # TODO: remove later
- @property
- def supported(self):
- debug("OBDCommand.supported is deprecated. Use OBD.supports() instead", True)
- return False
def __call__(self, messages):
@@ -95,7 +93,9 @@ def __call__(self, messages):
# and reference to original command
r = OBDResponse(self, messages)
if messages:
- r.value, r.unit = self.decode(messages)
+ r.value = self.decode(messages)
+ else:
+ logger.info(str(self) + " did not recieve any acceptable messages")
return r
@@ -106,9 +106,11 @@ def __constrain_message_data(self, message):
if len(message.data) > self.bytes:
# chop off the right side
message.data = message.data[:self.bytes]
- else:
+ logger.debug("Message was longer than expected. Trimmed message: " + repr(message.data))
+ elif len(message.data) < self.bytes:
# pad the right with zeros
message.data += (b'\x00' * (self.bytes - len(message.data)))
+ logger.debug("Message was shorter than expected. Padded message: " + repr(message.data))
def __str__(self):
diff --git a/obd/OBDResponse.py b/obd/OBDResponse.py
index 59148f72..e3c7d550 100644
--- a/obd/OBDResponse.py
+++ b/obd/OBDResponse.py
@@ -30,34 +30,12 @@
########################################################################
-
import time
+from .codes import *
+import logging
-
-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"
+logger = logging.getLogger(__name__)
@@ -68,17 +46,23 @@ 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()
+ @property
+ def unit(self):
+ # for backwards compatibility
+ if isinstance(self.value, Unit.Quantity):
+ return str(self.value.u)
+ elif self.value == None:
+ return None
+ else:
+ return str(type(self.value))
+
def is_null(self):
return (not self.messages) or (self.value == None)
def __str__(self):
- if self.unit != Unit.NONE:
- return "%s %s" % (str(self.value), str(self.unit))
- else:
- return str(self.value)
+ return str(self.value)
@@ -93,16 +77,93 @@ def __init__(self):
self.MIL = False
self.DTC_count = 0
self.ignition_type = ""
- self.tests = []
+ # make sure each test is available by name
+ # until real data comes it. This also prevents things from
+ # breaking when the user looks up a standard test that's null.
+ null_test = StatusTest()
+ for name in BASE_TESTS + SPARK_TESTS + COMPRESSION_TESTS:
+ if name: # filter out None/reserved tests
+ self.__dict__[name] = null_test
-class Test():
- def __init__(self, name, available, incomplete):
- self.name = name
- self.available = available
- self.incomplete = incomplete
+
+class StatusTest():
+ def __init__(self, name="", available=False, complete=False):
+ self.name = name
+ self.available = available
+ self.complete = complete
def __str__(self):
a = "Available" if self.available else "Unavailable"
- c = "Incomplete" if self.incomplete else "Complete"
+ c = "Complete" if self.complete else "Incomplete"
return "Test %s: %s, %s" % (self.name, a, c)
+
+
+class Monitor():
+ def __init__(self):
+ self._tests = {} # tid : MonitorTest
+
+ # make the standard TIDs available as null monitor tests
+ # until real data comes it. This also prevents things from
+ # breaking when the user looks up a standard test that's null.
+ null_test = MonitorTest()
+
+ for tid in TEST_IDS:
+ name = TEST_IDS[tid][0]
+ self.__dict__[name] = null_test
+ self._tests[tid] = null_test
+
+ def add_test(self, test):
+ self._tests[test.tid] = test
+ if test.name is not None:
+ self.__dict__[test.name] = test
+
+ @property
+ def tests(self):
+ return [test for test in self._tests.values() if not test.is_null()]
+
+ def __str__(self):
+ if len(self.tests) > 0:
+ return "\n".join([ str(t) for t in self.tests ])
+ else:
+ return "No tests to report"
+
+ def __len__(self):
+ return len(self.tests)
+
+ def __getitem__(self, key):
+ if isinstance(key, int):
+ return self._tests.get(key, MonitorTest())
+ elif isinstance(key, str) or isinstance(key, unicode):
+ return self.__dict__.get(key, MonitorTest())
+ else:
+ logger.warning("Monitor test results can only be retrieved by TID value or property name")
+
+
+
+class MonitorTest():
+ def __init__(self):
+ self.tid = None
+ self.name = None
+ self.desc = None
+ self.value = None
+ self.min = None
+ self.max = None
+
+ @property
+ def passed(self):
+ if not self.is_null():
+ return (self.value >= self.min) and (self.value <= self.max)
+ else:
+ return False
+
+ def is_null(self):
+ return (self.tid is None or
+ self.value is None or
+ self.min is None or
+ self.max is None)
+
+ def __str__(self):
+ return "%s : %s [%s]" % (self.desc,
+ str(self.value),
+ "PASSED" if self.passed else "FAILED")
diff --git a/obd/README.md b/obd/README.md
index 98aa3cc3..7788290e 100644
--- a/obd/README.md
+++ b/obd/README.md
@@ -1,18 +1,15 @@
-
-Notes
------
-
```
+ API
┌───────────────────────┐
-│ obd.py (API) │
+│ obd.py / async.py │
└───┰───────────────────┘
┃ ▲
┃ ┃
-┌───╂───────────────╂───┐ ┌─────────────────┐
-│ ┃ ┗━━━┿━━━━━━┥ │
-│ ┃ OBDCommand.py │ │ decoders.py │
-│ ┃ ┏━━━┿━━━━ ▶│ │
-└───╂───────────────╂───┘ └─────────────────┘
+┌───╂───────────────╂───┐ ┌─────────────────┐ ┌────────────────────┐
+│ ┃ ┗━━━┿━━━━━━┥ │◀ ━━━━━━━┥ │
+│ ┃ OBDCommand.py │ │ decoders.py │ (maybe) │ UnitsAndScaling.py │
+│ ┃ ┏━━━┿━━━━ ▶│ ┝━━━━━━━ ▶│ │
+└───╂───────────────╂───┘ └─────────────────┘ └────────────────────┘
┃ ┃
┃ ┃
┌───╂───────────────╂───┐ ┌─────────────────┐
@@ -25,4 +22,11 @@ Notes
┌───────────────────┸───┐
│ pyserial │
└───────────────────────┘
+ Serial Port
```
+
+Not pictured:
+
+- `commands.py` : defines the various OBD commands, and which decoder they use
+- `codes.py` : stores tables of standardized values needed by `decoders.py` (mostly check-engine codes)
+- `OBDResponse.py` : defines structures/objects returned by the API in response to a query.
diff --git a/obd/UnitsAndScaling.py b/obd/UnitsAndScaling.py
new file mode 100644
index 00000000..4fbe9f1e
--- /dev/null
+++ b/obd/UnitsAndScaling.py
@@ -0,0 +1,174 @@
+
+########################################################################
+# #
+# 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) #
+# #
+########################################################################
+# #
+# UnitsAndScaling.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 pint
+from .utils import *
+
+
+# export the unit registry
+Unit = pint.UnitRegistry()
+Unit.define("percent = [] = %")
+Unit.define("ratio = []")
+Unit.define("gps = gram / second = GPS = grams_per_second")
+Unit.define("lph = liter / hour = LPH = liters_per_hour")
+Unit.define("ppm = count / 1000000 = PPM = parts_per_million")
+
+
+
+class UAS():
+ """
+ Class for representing a Unit and Scale conversion
+ Used in the decoding of Mode 06 monitor responses
+ """
+
+ def __init__(self, signed, scale, unit, offset=0):
+ self.signed = signed
+ self.scale = scale
+ self.unit = unit
+ self.offset = offset
+
+ def __call__(self, _bytes):
+ value = bytes_to_int(_bytes)
+
+ if self.signed:
+ value = twos_comp(value, len(_bytes) * 8)
+
+ value *= self.scale
+ value += self.offset
+ return Unit.Quantity(value, self.unit)
+
+
+# dict for looking up standardized UAS IDs with conversion objects
+UAS_IDS = {
+ # unsigned -----------------------------------------
+ 0x01 : UAS(False, 1, Unit.count),
+ 0x02 : UAS(False, 0.1, Unit.count),
+ 0x03 : UAS(False, 0.01, Unit.count),
+ 0x04 : UAS(False, 0.001, Unit.count),
+ 0x05 : UAS(False, 0.0000305, Unit.count),
+ 0x06 : UAS(False, 0.000305, Unit.count),
+ 0x07 : UAS(False, 0.25, Unit.rpm),
+ 0x08 : UAS(False, 0.01, Unit.kph),
+ 0x09 : UAS(False, 1, Unit.kph),
+ 0x0A : UAS(False, 0.122, Unit.millivolt),
+ 0x0B : UAS(False, 0.001, Unit.volt),
+ 0x0C : UAS(False, 0.01, Unit.volt),
+ 0x0D : UAS(False, 0.00390625, Unit.milliampere),
+ 0x0E : UAS(False, 0.001, Unit.ampere),
+ 0x0F : UAS(False, 0.01, Unit.ampere),
+ 0x10 : UAS(False, 1, Unit.millisecond),
+ 0x11 : UAS(False, 100, Unit.millisecond),
+ 0x12 : UAS(False, 1, Unit.second),
+ 0x13 : UAS(False, 1, Unit.milliohm),
+ 0x14 : UAS(False, 1, Unit.ohm),
+ 0x15 : UAS(False, 1, Unit.kiloohm),
+ 0x16 : UAS(False, 0.1, Unit.celsius, offset=-40.0),
+ 0x17 : UAS(False, 0.01, Unit.kilopascal),
+ 0x18 : UAS(False, 0.0117, Unit.kilopascal),
+ 0x19 : UAS(False, 0.079, Unit.kilopascal),
+ 0x1A : UAS(False, 1, Unit.kilopascal),
+ 0x1B : UAS(False, 10, Unit.kilopascal),
+ 0x1C : UAS(False, 0.01, Unit.degree),
+ 0x1D : UAS(False, 0.5, Unit.degree),
+ 0x1E : UAS(False, 0.0000305, Unit.ratio),
+ 0x1F : UAS(False, 0.05, Unit.ratio),
+ 0x20 : UAS(False, 0.00390625, Unit.ratio),
+ 0x21 : UAS(False, 1, Unit.millihertz),
+ 0x22 : UAS(False, 1, Unit.hertz),
+ 0x23 : UAS(False, 1, Unit.kilohertz),
+ 0x24 : UAS(False, 1, Unit.count),
+ 0x25 : UAS(False, 1, Unit.kilometer),
+ 0x26 : UAS(False, 0.1, Unit.millivolt / Unit.millisecond),
+ 0x27 : UAS(False, 0.01, Unit.grams_per_second),
+ 0x28 : UAS(False, 1, Unit.grams_per_second),
+ 0x29 : UAS(False, 0.25, Unit.pascal / Unit.second),
+ 0x2A : UAS(False, 0.001, Unit.kilogram / Unit.hour),
+ 0x2B : UAS(False, 1, Unit.count),
+ 0x2C : UAS(False, 0.01, Unit.gram), # per-cylinder
+ 0x2D : UAS(False, 0.01, Unit.milligram), # per-stroke
+ 0x2E : lambda _bytes: any([ bool(x) for x in _bytes]),
+ 0x2F : UAS(False, 0.01, Unit.percent),
+ 0x30 : UAS(False, 0.001526, Unit.percent),
+ 0x31 : UAS(False, 0.001, Unit.liter),
+ 0x32 : UAS(False, 0.0000305, Unit.inch),
+ 0x33 : UAS(False, 0.00024414, Unit.ratio),
+ 0x34 : UAS(False, 1, Unit.minute),
+ 0x35 : UAS(False, 10, Unit.millisecond),
+ 0x36 : UAS(False, 0.01, Unit.gram),
+ 0x37 : UAS(False, 0.1, Unit.gram),
+ 0x38 : UAS(False, 1, Unit.gram),
+ 0x39 : UAS(False, 0.01, Unit.percent, offset=-327.68),
+ 0x3A : UAS(False, 0.001, Unit.gram),
+ 0x3B : UAS(False, 0.0001, Unit.gram),
+ 0x3C : UAS(False, 0.1, Unit.microsecond),
+ 0x3D : UAS(False, 0.01, Unit.milliampere),
+ 0x3E : UAS(False, 0.00006103516, Unit.millimeter ** 2),
+ 0x3F : UAS(False, 0.01, Unit.liter),
+ 0x40 : UAS(False, 1, Unit.ppm),
+ 0x41 : UAS(False, 0.01, Unit.microampere),
+
+ # signed -----------------------------------------
+ 0x81 : UAS(True, 1, Unit.count),
+ 0x82 : UAS(True, 0.1, Unit.count),
+ 0x83 : UAS(True, 0.01, Unit.count),
+ 0x84 : UAS(True, 0.001, Unit.count),
+ 0x85 : UAS(True, 0.0000305, Unit.count),
+ 0x86 : UAS(True, 0.000305, Unit.count),
+ 0x87 : UAS(True, 1, Unit.ppm),
+ #
+ 0x8A : UAS(True, 0.122, Unit.millivolt),
+ 0x8B : UAS(True, 0.001, Unit.volt),
+ 0x8C : UAS(True, 0.01, Unit.volt),
+ 0x8D : UAS(True, 0.00390625, Unit.milliampere),
+ 0x8E : UAS(True, 0.001, Unit.ampere),
+ #
+ 0x90 : UAS(True, 1, Unit.millisecond),
+ #
+ 0x96 : UAS(True, 0.1, Unit.celsius),
+ #
+ 0x99 : UAS(True, 0.1, Unit.kilopascal),
+ #
+ 0x9C : UAS(True, 0.01, Unit.degree),
+ 0x9D : UAS(True, 0.5, Unit.degree),
+ #
+ 0xA8 : UAS(True, 1, Unit.grams_per_second),
+ 0xA9 : UAS(True, 0.25, Unit.pascal / Unit.second),
+ #
+ 0xAD : UAS(True, 0.01, Unit.milligram), # per-stroke
+ 0xAE : UAS(True, 0.1, Unit.milligram), # per-stroke
+ 0xAF : UAS(True, 0.01, Unit.percent),
+ 0xB0 : UAS(True, 0.003052, Unit.percent),
+ 0xB1 : UAS(True, 2, Unit.millivolt / Unit.second),
+ #
+ 0xFC : UAS(True, 0.01, Unit.kilopascal),
+ 0xFD : UAS(True, 0.001, Unit.kilopascal),
+ 0xFE : UAS(True, 0.25, Unit.pascal),
+}
diff --git a/obd/__init__.py b/obd/__init__.py
index 51ea205c..dc96b281 100644
--- a/obd/__init__.py
+++ b/obd/__init__.py
@@ -41,7 +41,16 @@
from .async import Async
from .commands import commands
from .OBDCommand import OBDCommand
-from .OBDResponse import OBDResponse, Unit
+from .OBDResponse import OBDResponse
from .protocols import ECU
-from .utils import scan_serial, scanSerial, OBDStatus # TODO: scanSerial() deprecated
-from .debug import debug
+from .utils import scan_serial, OBDStatus
+from .UnitsAndScaling import Unit
+
+import logging
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.WARNING)
+
+console_handler = logging.StreamHandler() # sends output to stderr
+console_handler.setFormatter(logging.Formatter("[%(name)s] %(message)s"))
+logger.addHandler(console_handler)
diff --git a/obd/__version__.py b/obd/__version__.py
index fb8cecd5..1f199f19 100644
--- a/obd/__version__.py
+++ b/obd/__version__.py
@@ -1,2 +1,2 @@
-__version__ = '0.5.1'
+__version__ = '0.6.0'
diff --git a/obd/async.py b/obd/async.py
index 264600da..ecd43746 100644
--- a/obd/async.py
+++ b/obd/async.py
@@ -31,9 +31,12 @@
import time
import threading
+import logging
from .OBDResponse import OBDResponse
-from .debug import debug
-from . import OBD
+from .obd import OBD
+
+logger = logging.getLogger(__name__)
+
class Async(OBD):
"""
@@ -41,7 +44,7 @@ class Async(OBD):
Specialized for asynchronous value reporting.
"""
- def __init__(self, portstr=None, baudrate=38400, protocol=None, fast=True):
+ def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True):
super(Async, self).__init__(portstr, baudrate, protocol, fast)
self.__commands = {} # key = OBDCommand, value = Response
self.__callbacks = {} # key = OBDCommand, value = list of Functions
@@ -58,15 +61,15 @@ def running(self):
def start(self):
""" Starts the async update loop """
if not self.is_connected():
- debug("Async thread not started because no connection was made")
+ logger.info("Async thread not started because no connection was made")
return
if len(self.__commands) == 0:
- debug("Async thread not started because no commands were registered")
+ logger.info("Async thread not started because no commands were registered")
return
if self.__thread is None:
- debug("Starting async thread")
+ logger.info("Starting async thread")
self.__running = True
self.__thread = threading.Thread(target=self.run)
self.__thread.daemon = True
@@ -76,11 +79,11 @@ def start(self):
def stop(self):
""" Stops the async update loop """
if self.__thread is not None:
- debug("Stopping async thread...")
+ logger.info("Stopping async thread...")
self.__running = False
self.__thread.join()
self.__thread = None
- debug("Async thread stopped")
+ logger.info("Async thread stopped")
def paused(self):
@@ -130,22 +133,22 @@ def watch(self, c, callback=None, force=False):
# the dict shouldn't be changed while the daemon thread is iterating
if self.__running:
- debug("Can't watch() while running, please use stop()", True)
+ logger.warning("Can't watch() while running, please use stop()")
else:
- if not force and not self.supports(c):
- debug("'%s' is not supported" % str(c), True)
+ if not force and not self.test_cmd(c):
+ # self.test_cmd() will print warnings
return
# new command being watched, store the command
if c not in self.__commands:
- debug("Watching command: %s" % str(c))
+ logger.info("Watching command: %s" % str(c))
self.__commands[c] = OBDResponse() # give it an initial value
self.__callbacks[c] = [] # create an empty list
# if a callback was given, push it
if hasattr(callback, "__call__") and (callback not in self.__callbacks[c]):
- debug("subscribing callback for command: %s" % str(c))
+ logger.info("subscribing callback for command: %s" % str(c))
self.__callbacks[c].append(callback)
@@ -158,9 +161,9 @@ def unwatch(self, c, callback=None):
# the dict shouldn't be changed while the daemon thread is iterating
if self.__running:
- debug("Can't unwatch() while running, please use stop()", True)
+ logger.warning("Can't unwatch() while running, please use stop()")
else:
- debug("Unwatching command: %s" % str(c))
+ logger.info("Unwatching command: %s" % str(c))
if c in self.__commands:
# if a callback was specified, only remove the callback
@@ -181,9 +184,9 @@ def unwatch_all(self):
# the dict shouldn't be changed while the daemon thread is iterating
if self.__running:
- debug("Can't unwatch_all() while running, please use stop()", True)
+ logger.warning("Can't unwatch_all() while running, please use stop()")
else:
- debug("Unwatching all")
+ logger.info("Unwatching all")
self.__commands = {}
self.__callbacks = {}
diff --git a/obd/codes.py b/obd/codes.py
index 8278d922..97202e92 100644
--- a/obd/codes.py
+++ b/obd/codes.py
@@ -2101,30 +2101,36 @@
}
IGNITION_TYPE = [
- "Spark",
- "Compression",
+ "spark",
+ "compression",
+]
+
+BASE_TESTS = [
+ "MISFIRE_MONITORING",
+ "FUEL_SYSTEM_MONITORING",
+ "COMPONENT_MONITORING",
]
SPARK_TESTS = [
- "EGR System",
- "Oxygen Sensor Heater",
- "Oxygen Sensor",
- "A/C Refrigerant",
- "Secondary Air System",
- "Evaporative System",
- "Heated Catalyst",
- "Catalyst",
+ "CATALYST_MONITORING",
+ "HEATED_CATALYST_MONITORING",
+ "EVAPORATIVE_SYSTEM_MONITORING",
+ "SECONDARY_AIR_SYSTEM_MONITORING",
+ None,
+ "OXYGEN_SENSOR_MONITORING",
+ "OXYGEN_SENSOR_HEATER_MONITORING",
+ "EGR_VVT_SYSTEM_MONITORING"
]
COMPRESSION_TESTS = [
- "EGR and/or VVT System",
- "PM filter monitoring",
- "Exhaust Gas Sensor",
- "None",
- "Boost Pressure",
- "None",
- "NOx/SCR Monitor",
- "NMHC Catalyst",
+ "NMHC_CATALYST_MONITORING",
+ "NOX_SCR_AFTERTREATMENT_MONITORING",
+ None,
+ "BOOST_PRESSURE_MONITORING",
+ None,
+ "EXHAUST_GAS_SENSOR_MONITORING",
+ "PM_FILTER_MONITORING",
+ "EGR_VVT_SYSTEM_MONITORING",
]
FUEL_STATUS = [
@@ -2205,3 +2211,20 @@
"Hybrid Regenerative",
"Bifuel running diesel",
]
+
+TEST_IDS = {
+ # :
+ # 0x0 is reserved
+ 0x01 : ("RTL_THRESHOLD_VOLTAGE", "Rich to lean sensor threshold voltage"),
+ 0x02 : ("LTR_THRESHOLD_VOLTAGE", "Lean to rich sensor threshold voltage"),
+ 0x03 : ("LOW_VOLTAGE_SWITCH_TIME", "Low sensor voltage for switch time calculation"),
+ 0x04 : ("HIGH_VOLTAGE_SWITCH_TIME", "High sensor voltage for switch time calculation"),
+ 0x05 : ("RTL_SWITCH_TIME", "Rich to lean sensor switch time"),
+ 0x06 : ("LTR_SWITCH_TIME", "Lean to rich sensor switch time"),
+ 0x07 : ("MIN_VOLTAGE", "Minimum sensor voltage for test cycle"),
+ 0x08 : ("MAX_VOLTAGE", "Maximum sensor voltage for test cycle"),
+ 0x09 : ("TRANSITION_TIME", "Time between sensor transitions"),
+ 0x0A : ("SENSOR_PERIOD", "Sensor period"),
+ 0x0B : ("MISFIRE_AVERAGE", "Average misfire counts for last ten driving cycles"),
+ 0x0C : ("MISFIRE_COUNT", "Misfire counts for last/current driving cycles"),
+}
diff --git a/obd/commands.py b/obd/commands.py
index e166d4a5..b5735ce2 100644
--- a/obd/commands.py
+++ b/obd/commands.py
@@ -32,8 +32,10 @@
from .protocols import ECU
from .OBDCommand import OBDCommand
from .decoders import *
-from .debug import debug
+import logging
+
+logger = logging.getLogger(__name__)
@@ -48,106 +50,106 @@
__mode1__ = [
# name description cmd bytes decoder ECU fast
- OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , "0100", 4, pid, ECU.ENGINE, True),
- OBDCommand("STATUS" , "Status since DTCs cleared" , "0101", 4, status, ECU.ENGINE, True),
- OBDCommand("FREEZE_DTC" , "Freeze DTC" , "0102", 2, drop, ECU.ENGINE, True),
- OBDCommand("FUEL_STATUS" , "Fuel System Status" , "0103", 2, fuel_status, ECU.ENGINE, True),
- OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , "0104", 1, percent, ECU.ENGINE, True),
- OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , "0105", 1, temp, ECU.ENGINE, True),
- OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , "0106", 1, percent_centered, ECU.ENGINE, True),
- OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , "0107", 1, percent_centered, ECU.ENGINE, True),
- OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , "0108", 1, percent_centered, ECU.ENGINE, True),
- OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , "0109", 1, percent_centered, ECU.ENGINE, True),
- OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , "010A", 1, fuel_pressure, ECU.ENGINE, True),
- OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , "010B", 1, pressure, ECU.ENGINE, True),
- OBDCommand("RPM" , "Engine RPM" , "010C", 2, rpm, ECU.ENGINE, True),
- OBDCommand("SPEED" , "Vehicle Speed" , "010D", 1, speed, ECU.ENGINE, True),
- OBDCommand("TIMING_ADVANCE" , "Timing Advance" , "010E", 1, timing_advance, ECU.ENGINE, True),
- OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , "010F", 1, temp, ECU.ENGINE, True),
- OBDCommand("MAF" , "Air Flow Rate (MAF)" , "0110", 2, maf, ECU.ENGINE, True),
- OBDCommand("THROTTLE_POS" , "Throttle Position" , "0111", 1, percent, ECU.ENGINE, True),
- OBDCommand("AIR_STATUS" , "Secondary Air Status" , "0112", 1, air_status, ECU.ENGINE, True),
- OBDCommand("O2_SENSORS" , "O2 Sensors Present" , "0113", 1, drop, ECU.ENGINE, True),
- OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , "0114", 2, sensor_voltage, ECU.ENGINE, True),
- OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , "0115", 2, sensor_voltage, ECU.ENGINE, True),
- OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , "0116", 2, sensor_voltage, ECU.ENGINE, True),
- OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , "0117", 2, sensor_voltage, ECU.ENGINE, True),
- OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , "0118", 2, sensor_voltage, ECU.ENGINE, True),
- OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , "0119", 2, sensor_voltage, ECU.ENGINE, True),
- OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , "011A", 2, sensor_voltage, ECU.ENGINE, True),
- OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , "011B", 2, sensor_voltage, ECU.ENGINE, True),
- OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , "011C", 1, obd_compliance, ECU.ENGINE, True),
- OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , "011D", 1, drop, ECU.ENGINE, True),
- OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status" , "011E", 1, drop, ECU.ENGINE, True),
- OBDCommand("RUN_TIME" , "Engine Run Time" , "011F", 2, seconds, ECU.ENGINE, True),
+ OBDCommand("PIDS_A" , "Supported PIDs [01-20]" , b"0100", 4, pid, ECU.ENGINE, True),
+ OBDCommand("STATUS" , "Status since DTCs cleared" , b"0101", 4, status, ECU.ENGINE, True),
+ OBDCommand("FREEZE_DTC" , "DTC that triggered the freeze frame" , b"0102", 2, single_dtc, ECU.ENGINE, True),
+ OBDCommand("FUEL_STATUS" , "Fuel System Status" , b"0103", 2, fuel_status, ECU.ENGINE, True),
+ OBDCommand("ENGINE_LOAD" , "Calculated Engine Load" , b"0104", 1, percent, ECU.ENGINE, True),
+ OBDCommand("COOLANT_TEMP" , "Engine Coolant Temperature" , b"0105", 1, temp, ECU.ENGINE, True),
+ OBDCommand("SHORT_FUEL_TRIM_1" , "Short Term Fuel Trim - Bank 1" , b"0106", 1, percent_centered, ECU.ENGINE, True),
+ OBDCommand("LONG_FUEL_TRIM_1" , "Long Term Fuel Trim - Bank 1" , b"0107", 1, percent_centered, ECU.ENGINE, True),
+ OBDCommand("SHORT_FUEL_TRIM_2" , "Short Term Fuel Trim - Bank 2" , b"0108", 1, percent_centered, ECU.ENGINE, True),
+ OBDCommand("LONG_FUEL_TRIM_2" , "Long Term Fuel Trim - Bank 2" , b"0109", 1, percent_centered, ECU.ENGINE, True),
+ OBDCommand("FUEL_PRESSURE" , "Fuel Pressure" , b"010A", 1, fuel_pressure, ECU.ENGINE, True),
+ OBDCommand("INTAKE_PRESSURE" , "Intake Manifold Pressure" , b"010B", 1, pressure, ECU.ENGINE, True),
+ OBDCommand("RPM" , "Engine RPM" , b"010C", 2, uas(0x07), ECU.ENGINE, True),
+ OBDCommand("SPEED" , "Vehicle Speed" , b"010D", 1, uas(0x09), ECU.ENGINE, True),
+ OBDCommand("TIMING_ADVANCE" , "Timing Advance" , b"010E", 1, timing_advance, ECU.ENGINE, True),
+ OBDCommand("INTAKE_TEMP" , "Intake Air Temp" , b"010F", 1, temp, ECU.ENGINE, True),
+ OBDCommand("MAF" , "Air Flow Rate (MAF)" , b"0110", 2, uas(0x27), ECU.ENGINE, True),
+ OBDCommand("THROTTLE_POS" , "Throttle Position" , b"0111", 1, percent, ECU.ENGINE, True),
+ OBDCommand("AIR_STATUS" , "Secondary Air Status" , b"0112", 1, air_status, ECU.ENGINE, True),
+ OBDCommand("O2_SENSORS" , "O2 Sensors Present" , b"0113", 1, o2_sensors, ECU.ENGINE, True),
+ OBDCommand("O2_B1S1" , "O2: Bank 1 - Sensor 1 Voltage" , b"0114", 2, sensor_voltage, ECU.ENGINE, True),
+ OBDCommand("O2_B1S2" , "O2: Bank 1 - Sensor 2 Voltage" , b"0115", 2, sensor_voltage, ECU.ENGINE, True),
+ OBDCommand("O2_B1S3" , "O2: Bank 1 - Sensor 3 Voltage" , b"0116", 2, sensor_voltage, ECU.ENGINE, True),
+ OBDCommand("O2_B1S4" , "O2: Bank 1 - Sensor 4 Voltage" , b"0117", 2, sensor_voltage, ECU.ENGINE, True),
+ OBDCommand("O2_B2S1" , "O2: Bank 2 - Sensor 1 Voltage" , b"0118", 2, sensor_voltage, ECU.ENGINE, True),
+ OBDCommand("O2_B2S2" , "O2: Bank 2 - Sensor 2 Voltage" , b"0119", 2, sensor_voltage, ECU.ENGINE, True),
+ OBDCommand("O2_B2S3" , "O2: Bank 2 - Sensor 3 Voltage" , b"011A", 2, sensor_voltage, ECU.ENGINE, True),
+ OBDCommand("O2_B2S4" , "O2: Bank 2 - Sensor 4 Voltage" , b"011B", 2, sensor_voltage, ECU.ENGINE, True),
+ OBDCommand("OBD_COMPLIANCE" , "OBD Standards Compliance" , b"011C", 1, obd_compliance, ECU.ENGINE, True),
+ OBDCommand("O2_SENSORS_ALT" , "O2 Sensors Present (alternate)" , b"011D", 1, o2_sensors_alt, ECU.ENGINE, True),
+ OBDCommand("AUX_INPUT_STATUS" , "Auxiliary input status (power take off)" , b"011E", 1, aux_input_status, ECU.ENGINE, True),
+ OBDCommand("RUN_TIME" , "Engine Run Time" , b"011F", 2, uas(0x12), ECU.ENGINE, True),
# name description cmd bytes decoder ECU fast
- OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , "0120", 4, pid, ECU.ENGINE, True),
- OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , "0121", 2, distance, ECU.ENGINE, True),
- OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , "0122", 2, fuel_pres_vac, ECU.ENGINE, True),
- OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , "0123", 2, fuel_pres_direct, ECU.ENGINE, True),
- OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , "0124", 4, sensor_voltage_big, ECU.ENGINE, True),
- OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , "0125", 4, sensor_voltage_big, ECU.ENGINE, True),
- OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , "0126", 4, sensor_voltage_big, ECU.ENGINE, True),
- OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , "0127", 4, sensor_voltage_big, ECU.ENGINE, True),
- OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , "0128", 4, sensor_voltage_big, ECU.ENGINE, True),
- OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , "0129", 4, sensor_voltage_big, ECU.ENGINE, True),
- OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , "012A", 4, sensor_voltage_big, ECU.ENGINE, True),
- OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , "012B", 4, sensor_voltage_big, ECU.ENGINE, True),
- OBDCommand("COMMANDED_EGR" , "Commanded EGR" , "012C", 1, percent, ECU.ENGINE, True),
- OBDCommand("EGR_ERROR" , "EGR Error" , "012D", 1, percent_centered, ECU.ENGINE, True),
- OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , "012E", 1, percent, ECU.ENGINE, True),
- OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , "012F", 1, percent, ECU.ENGINE, True),
- OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , "0130", 1, count, ECU.ENGINE, True),
- OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , "0131", 2, distance, ECU.ENGINE, True),
- OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , "0132", 2, evap_pressure, ECU.ENGINE, True),
- OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , "0133", 1, pressure, ECU.ENGINE, True),
- OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , "0134", 4, current_centered, ECU.ENGINE, True),
- OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , "0135", 4, current_centered, ECU.ENGINE, True),
- OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , "0136", 4, current_centered, ECU.ENGINE, True),
- OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , "0137", 4, current_centered, ECU.ENGINE, True),
- OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , "0138", 4, current_centered, ECU.ENGINE, True),
- OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , "0139", 4, current_centered, ECU.ENGINE, True),
- OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , "013A", 4, current_centered, ECU.ENGINE, True),
- OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , "013B", 4, current_centered, ECU.ENGINE, True),
- OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , "013C", 2, catalyst_temp, ECU.ENGINE, True),
- OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , "013D", 2, catalyst_temp, ECU.ENGINE, True),
- OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , "013E", 2, catalyst_temp, ECU.ENGINE, True),
- OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , "013F", 2, catalyst_temp, ECU.ENGINE, True),
+ OBDCommand("PIDS_B" , "Supported PIDs [21-40]" , b"0120", 4, pid, ECU.ENGINE, True),
+ OBDCommand("DISTANCE_W_MIL" , "Distance Traveled with MIL on" , b"0121", 2, uas(0x25), ECU.ENGINE, True),
+ OBDCommand("FUEL_RAIL_PRESSURE_VAC" , "Fuel Rail Pressure (relative to vacuum)" , b"0122", 2, uas(0x19), ECU.ENGINE, True),
+ OBDCommand("FUEL_RAIL_PRESSURE_DIRECT" , "Fuel Rail Pressure (direct inject)" , b"0123", 2, uas(0x1B), ECU.ENGINE, True),
+ OBDCommand("O2_S1_WR_VOLTAGE" , "02 Sensor 1 WR Lambda Voltage" , b"0124", 4, sensor_voltage_big, ECU.ENGINE, True),
+ OBDCommand("O2_S2_WR_VOLTAGE" , "02 Sensor 2 WR Lambda Voltage" , b"0125", 4, sensor_voltage_big, ECU.ENGINE, True),
+ OBDCommand("O2_S3_WR_VOLTAGE" , "02 Sensor 3 WR Lambda Voltage" , b"0126", 4, sensor_voltage_big, ECU.ENGINE, True),
+ OBDCommand("O2_S4_WR_VOLTAGE" , "02 Sensor 4 WR Lambda Voltage" , b"0127", 4, sensor_voltage_big, ECU.ENGINE, True),
+ OBDCommand("O2_S5_WR_VOLTAGE" , "02 Sensor 5 WR Lambda Voltage" , b"0128", 4, sensor_voltage_big, ECU.ENGINE, True),
+ OBDCommand("O2_S6_WR_VOLTAGE" , "02 Sensor 6 WR Lambda Voltage" , b"0129", 4, sensor_voltage_big, ECU.ENGINE, True),
+ OBDCommand("O2_S7_WR_VOLTAGE" , "02 Sensor 7 WR Lambda Voltage" , b"012A", 4, sensor_voltage_big, ECU.ENGINE, True),
+ OBDCommand("O2_S8_WR_VOLTAGE" , "02 Sensor 8 WR Lambda Voltage" , b"012B", 4, sensor_voltage_big, ECU.ENGINE, True),
+ OBDCommand("COMMANDED_EGR" , "Commanded EGR" , b"012C", 1, percent, ECU.ENGINE, True),
+ OBDCommand("EGR_ERROR" , "EGR Error" , b"012D", 1, percent_centered, ECU.ENGINE, True),
+ OBDCommand("EVAPORATIVE_PURGE" , "Commanded Evaporative Purge" , b"012E", 1, percent, ECU.ENGINE, True),
+ OBDCommand("FUEL_LEVEL" , "Fuel Level Input" , b"012F", 1, percent, ECU.ENGINE, True),
+ OBDCommand("WARMUPS_SINCE_DTC_CLEAR" , "Number of warm-ups since codes cleared" , b"0130", 1, uas(0x01), ECU.ENGINE, True),
+ OBDCommand("DISTANCE_SINCE_DTC_CLEAR" , "Distance traveled since codes cleared" , b"0131", 2, uas(0x25), ECU.ENGINE, True),
+ OBDCommand("EVAP_VAPOR_PRESSURE" , "Evaporative system vapor pressure" , b"0132", 2, evap_pressure, ECU.ENGINE, True),
+ OBDCommand("BAROMETRIC_PRESSURE" , "Barometric Pressure" , b"0133", 1, pressure, ECU.ENGINE, True),
+ OBDCommand("O2_S1_WR_CURRENT" , "02 Sensor 1 WR Lambda Current" , b"0134", 4, current_centered, ECU.ENGINE, True),
+ OBDCommand("O2_S2_WR_CURRENT" , "02 Sensor 2 WR Lambda Current" , b"0135", 4, current_centered, ECU.ENGINE, True),
+ OBDCommand("O2_S3_WR_CURRENT" , "02 Sensor 3 WR Lambda Current" , b"0136", 4, current_centered, ECU.ENGINE, True),
+ OBDCommand("O2_S4_WR_CURRENT" , "02 Sensor 4 WR Lambda Current" , b"0137", 4, current_centered, ECU.ENGINE, True),
+ OBDCommand("O2_S5_WR_CURRENT" , "02 Sensor 5 WR Lambda Current" , b"0138", 4, current_centered, ECU.ENGINE, True),
+ OBDCommand("O2_S6_WR_CURRENT" , "02 Sensor 6 WR Lambda Current" , b"0139", 4, current_centered, ECU.ENGINE, True),
+ OBDCommand("O2_S7_WR_CURRENT" , "02 Sensor 7 WR Lambda Current" , b"013A", 4, current_centered, ECU.ENGINE, True),
+ OBDCommand("O2_S8_WR_CURRENT" , "02 Sensor 8 WR Lambda Current" , b"013B", 4, current_centered, ECU.ENGINE, True),
+ OBDCommand("CATALYST_TEMP_B1S1" , "Catalyst Temperature: Bank 1 - Sensor 1" , b"013C", 2, uas(0x16), ECU.ENGINE, True),
+ OBDCommand("CATALYST_TEMP_B2S1" , "Catalyst Temperature: Bank 2 - Sensor 1" , b"013D", 2, uas(0x16), ECU.ENGINE, True),
+ OBDCommand("CATALYST_TEMP_B1S2" , "Catalyst Temperature: Bank 1 - Sensor 2" , b"013E", 2, uas(0x16), ECU.ENGINE, True),
+ OBDCommand("CATALYST_TEMP_B2S2" , "Catalyst Temperature: Bank 2 - Sensor 2" , b"013F", 2, uas(0x16), ECU.ENGINE, True),
# name description cmd bytes decoder ECU fast
- OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , "0140", 4, pid, ECU.ENGINE, True),
- OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , "0141", 4, drop, ECU.ENGINE, True),
- OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , "0142", 2, drop, ECU.ENGINE, True),
- OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , "0143", 2, drop, ECU.ENGINE, True),
- OBDCommand("COMMAND_EQUIV_RATIO" , "Command equivalence ratio" , "0144", 2, drop, ECU.ENGINE, True),
- OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , "0145", 1, percent, ECU.ENGINE, True),
- OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , "0146", 1, temp, ECU.ENGINE, True),
- OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , "0147", 1, percent, ECU.ENGINE, True),
- OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , "0148", 1, percent, ECU.ENGINE, True),
- OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , "0149", 1, percent, ECU.ENGINE, True),
- OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , "014A", 1, percent, ECU.ENGINE, True),
- OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , "014B", 1, percent, ECU.ENGINE, True),
- OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , "014C", 1, percent, ECU.ENGINE, True),
- OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , "014D", 2, minutes, ECU.ENGINE, True),
- OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , "014E", 2, minutes, ECU.ENGINE, True),
- OBDCommand("MAX_VALUES" , "Various Max values" , "014F", 4, drop, ECU.ENGINE, True), # todo: decode this
- OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , "0150", 4, max_maf, ECU.ENGINE, True),
- OBDCommand("FUEL_TYPE" , "Fuel Type" , "0151", 1, fuel_type, ECU.ENGINE, True),
- OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , "0152", 1, percent, ECU.ENGINE, True),
- OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , "0153", 2, abs_evap_pressure, ECU.ENGINE, True),
- OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , "0154", 2, evap_pressure_alt, ECU.ENGINE, True),
- OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , "0155", 2, percent_centered, ECU.ENGINE, True), # todo: decode seconds value for banks 3 and 4
- OBDCommand("LONG_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , "0156", 2, percent_centered, ECU.ENGINE, True),
- OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , "0157", 2, percent_centered, ECU.ENGINE, True),
- OBDCommand("LONG_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , "0158", 2, percent_centered, ECU.ENGINE, True),
- OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , "0159", 2, fuel_pres_direct, ECU.ENGINE, True),
- OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , "015A", 1, percent, ECU.ENGINE, True),
- OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , "015B", 1, percent, ECU.ENGINE, True),
- OBDCommand("OIL_TEMP" , "Engine oil temperature" , "015C", 1, temp, ECU.ENGINE, True),
- OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , "015D", 2, inject_timing, ECU.ENGINE, True),
- OBDCommand("FUEL_RATE" , "Engine fuel rate" , "015E", 2, fuel_rate, ECU.ENGINE, True),
- OBDCommand("EMISSION_REQ" , "Designed emission requirements" , "015F", 1, drop, ECU.ENGINE, True),
+ OBDCommand("PIDS_C" , "Supported PIDs [41-60]" , b"0140", 4, pid, ECU.ENGINE, True),
+ OBDCommand("STATUS_DRIVE_CYCLE" , "Monitor status this drive cycle" , b"0141", 4, status, ECU.ENGINE, True),
+ OBDCommand("CONTROL_MODULE_VOLTAGE" , "Control module voltage" , b"0142", 2, uas(0x0B), ECU.ENGINE, True),
+ OBDCommand("ABSOLUTE_LOAD" , "Absolute load value" , b"0143", 2, absolute_load, ECU.ENGINE, True),
+ OBDCommand("COMMANDED_EQUIV_RATIO" , "Commanded equivalence ratio" , b"0144", 2, uas(0x1E), ECU.ENGINE, True),
+ OBDCommand("RELATIVE_THROTTLE_POS" , "Relative throttle position" , b"0145", 1, percent, ECU.ENGINE, True),
+ OBDCommand("AMBIANT_AIR_TEMP" , "Ambient air temperature" , b"0146", 1, temp, ECU.ENGINE, True),
+ OBDCommand("THROTTLE_POS_B" , "Absolute throttle position B" , b"0147", 1, percent, ECU.ENGINE, True),
+ OBDCommand("THROTTLE_POS_C" , "Absolute throttle position C" , b"0148", 1, percent, ECU.ENGINE, True),
+ OBDCommand("ACCELERATOR_POS_D" , "Accelerator pedal position D" , b"0149", 1, percent, ECU.ENGINE, True),
+ OBDCommand("ACCELERATOR_POS_E" , "Accelerator pedal position E" , b"014A", 1, percent, ECU.ENGINE, True),
+ OBDCommand("ACCELERATOR_POS_F" , "Accelerator pedal position F" , b"014B", 1, percent, ECU.ENGINE, True),
+ OBDCommand("THROTTLE_ACTUATOR" , "Commanded throttle actuator" , b"014C", 1, percent, ECU.ENGINE, True),
+ OBDCommand("RUN_TIME_MIL" , "Time run with MIL on" , b"014D", 2, uas(0x34), ECU.ENGINE, True),
+ OBDCommand("TIME_SINCE_DTC_CLEARED" , "Time since trouble codes cleared" , b"014E", 2, uas(0x34), ECU.ENGINE, True),
+ OBDCommand("MAX_VALUES" , "Various Max values" , b"014F", 4, drop, ECU.ENGINE, True), # todo: decode this
+ OBDCommand("MAX_MAF" , "Maximum value for mass air flow sensor" , b"0150", 4, max_maf, ECU.ENGINE, True),
+ OBDCommand("FUEL_TYPE" , "Fuel Type" , b"0151", 1, fuel_type, ECU.ENGINE, True),
+ OBDCommand("ETHANOL_PERCENT" , "Ethanol Fuel Percent" , b"0152", 1, percent, ECU.ENGINE, True),
+ OBDCommand("EVAP_VAPOR_PRESSURE_ABS" , "Absolute Evap system Vapor Pressure" , b"0153", 2, abs_evap_pressure, ECU.ENGINE, True),
+ OBDCommand("EVAP_VAPOR_PRESSURE_ALT" , "Evap system vapor pressure" , b"0154", 2, evap_pressure_alt, ECU.ENGINE, True),
+ OBDCommand("SHORT_O2_TRIM_B1" , "Short term secondary O2 trim - Bank 1" , b"0155", 2, percent_centered, ECU.ENGINE, True), # todo: decode seconds value for banks 3 and 4
+ OBDCommand("LONG_O2_TRIM_B1" , "Long term secondary O2 trim - Bank 1" , b"0156", 2, percent_centered, ECU.ENGINE, True),
+ OBDCommand("SHORT_O2_TRIM_B2" , "Short term secondary O2 trim - Bank 2" , b"0157", 2, percent_centered, ECU.ENGINE, True),
+ OBDCommand("LONG_O2_TRIM_B2" , "Long term secondary O2 trim - Bank 2" , b"0158", 2, percent_centered, ECU.ENGINE, True),
+ OBDCommand("FUEL_RAIL_PRESSURE_ABS" , "Fuel rail pressure (absolute)" , b"0159", 2, uas(0x1B), ECU.ENGINE, True),
+ OBDCommand("RELATIVE_ACCEL_POS" , "Relative accelerator pedal position" , b"015A", 1, percent, ECU.ENGINE, True),
+ OBDCommand("HYBRID_BATTERY_REMAINING" , "Hybrid battery pack remaining life" , b"015B", 1, percent, ECU.ENGINE, True),
+ OBDCommand("OIL_TEMP" , "Engine oil temperature" , b"015C", 1, temp, ECU.ENGINE, True),
+ OBDCommand("FUEL_INJECT_TIMING" , "Fuel injection timing" , b"015D", 2, inject_timing, ECU.ENGINE, True),
+ OBDCommand("FUEL_RATE" , "Engine fuel rate" , b"015E", 2, fuel_rate, ECU.ENGINE, True),
+ OBDCommand("EMISSION_REQ" , "Designed emission requirements" , b"015F", 1, drop, ECU.ENGINE, True),
]
@@ -155,31 +157,144 @@
__mode2__ = []
for c in __mode1__:
c = c.clone()
- c.command = "02" + c.command[2:] # change the mode: 0100 ---> 0200
+ c.command = b"02" + c.command[2:] # change the mode: 0100 ---> 0200
c.name = "DTC_" + c.name
c.desc = "DTC " + c.desc
+ if c.decode == pid:
+ c.decode = drop # Never send mode 02 pid requests (use mode 01 instead)
__mode2__.append(c)
__mode3__ = [
# name description cmd bytes decoder ECU fast
- OBDCommand("GET_DTC" , "Get DTCs" , "03", 0, dtc, ECU.ALL, False),
+ OBDCommand("GET_DTC" , "Get DTCs" , b"03", 0, dtc, ECU.ALL, False),
]
__mode4__ = [
# name description cmd bytes decoder ECU fast
- OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , "04", 0, drop, ECU.ALL, False),
+ OBDCommand("CLEAR_DTC" , "Clear DTCs and Freeze data" , b"04", 0, drop, ECU.ALL, False),
+]
+
+__mode6__ = [
+ # Mode 06 calls PID's MID's (Monitor ID)
+ # This is for CAN only
+ # name description cmd bytes decoder ECU fast
+ OBDCommand("MIDS_A" , "Supported MIDs [01-20]" , b"0600", 0, pid, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_B1S1" , "O2 Sensor Monitor Bank 1 - Sensor 1" , b"0601", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_B1S2" , "O2 Sensor Monitor Bank 1 - Sensor 2" , b"0602", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_B1S3" , "O2 Sensor Monitor Bank 1 - Sensor 3" , b"0603", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_B1S4" , "O2 Sensor Monitor Bank 1 - Sensor 4" , b"0604", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_B2S1" , "O2 Sensor Monitor Bank 2 - Sensor 1" , b"0605", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_B2S2" , "O2 Sensor Monitor Bank 2 - Sensor 2" , b"0606", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_B2S3" , "O2 Sensor Monitor Bank 2 - Sensor 3" , b"0607", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_B2S4" , "O2 Sensor Monitor Bank 2 - Sensor 4" , b"0608", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_B3S1" , "O2 Sensor Monitor Bank 3 - Sensor 1" , b"0609", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_B3S2" , "O2 Sensor Monitor Bank 3 - Sensor 2" , b"060A", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_B3S3" , "O2 Sensor Monitor Bank 3 - Sensor 3" , b"060B", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_B3S4" , "O2 Sensor Monitor Bank 3 - Sensor 4" , b"060C", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_B4S1" , "O2 Sensor Monitor Bank 4 - Sensor 1" , b"060D", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_B4S2" , "O2 Sensor Monitor Bank 4 - Sensor 2" , b"060E", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_B4S3" , "O2 Sensor Monitor Bank 4 - Sensor 3" , b"060F", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_B4S4" , "O2 Sensor Monitor Bank 4 - Sensor 4" , b"0610", 0, monitor, ECU.ALL, False),
+] + ([None] * 15) + [ # 11 - 1F Reserved
+ OBDCommand("MIDS_B" , "Supported MIDs [21-40]" , b"0620", 0, pid, ECU.ALL, False),
+ OBDCommand("MONITOR_CATALYST_B1" , "Catalyst Monitor Bank 1" , b"0621", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_CATALYST_B2" , "Catalyst Monitor Bank 2" , b"0622", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_CATALYST_B3" , "Catalyst Monitor Bank 3" , b"0623", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_CATALYST_B4" , "Catalyst Monitor Bank 4" , b"0624", 0, monitor, ECU.ALL, False),
+] + ([None] * 12) + [ # 25 - 30 Reserved
+ OBDCommand("MONITOR_EGR_B1" , "EGR Monitor Bank 1" , b"0631", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_EGR_B2" , "EGR Monitor Bank 2" , b"0632", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_EGR_B3" , "EGR Monitor Bank 3" , b"0633", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_EGR_B4" , "EGR Monitor Bank 4" , b"0634", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_VVT_B1" , "VVT Monitor Bank 1" , b"0635", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_VVT_B2" , "VVT Monitor Bank 2" , b"0636", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_VVT_B3" , "VVT Monitor Bank 3" , b"0637", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_VVT_B4" , "VVT Monitor Bank 4" , b"0638", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_EVAP_150" , "EVAP Monitor (Cap Off / 0.150\")" , b"0639", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_EVAP_090" , "EVAP Monitor (0.090\")" , b"063A", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_EVAP_040" , "EVAP Monitor (0.040\")" , b"063B", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_EVAP_020" , "EVAP Monitor (0.020\")" , b"063C", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_PURGE_FLOW" , "Purge Flow Monitor" , b"063D", 0, monitor, ECU.ALL, False),
+] + ([None] * 2) + [ # 3E - 3F Reserved
+ OBDCommand("MIDS_C" , "Supported MIDs [41-60]" , b"0640", 0, pid, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_HEATER_B1S1" , "O2 Sensor Heater Monitor Bank 1 - Sensor 1" , b"0641", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_HEATER_B1S2" , "O2 Sensor Heater Monitor Bank 1 - Sensor 2" , b"0642", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_HEATER_B1S3" , "O2 Sensor Heater Monitor Bank 1 - Sensor 3" , b"0643", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_HEATER_B1S4" , "O2 Sensor Heater Monitor Bank 1 - Sensor 4" , b"0644", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_HEATER_B2S1" , "O2 Sensor Heater Monitor Bank 2 - Sensor 1" , b"0645", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_HEATER_B2S2" , "O2 Sensor Heater Monitor Bank 2 - Sensor 2" , b"0646", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_HEATER_B2S3" , "O2 Sensor Heater Monitor Bank 2 - Sensor 3" , b"0647", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_HEATER_B2S4" , "O2 Sensor Heater Monitor Bank 2 - Sensor 4" , b"0648", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_HEATER_B3S1" , "O2 Sensor Heater Monitor Bank 3 - Sensor 1" , b"0649", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_HEATER_B3S2" , "O2 Sensor Heater Monitor Bank 3 - Sensor 2" , b"064A", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_HEATER_B3S3" , "O2 Sensor Heater Monitor Bank 3 - Sensor 3" , b"064B", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_HEATER_B3S4" , "O2 Sensor Heater Monitor Bank 3 - Sensor 4" , b"064C", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_HEATER_B4S1" , "O2 Sensor Heater Monitor Bank 4 - Sensor 1" , b"064D", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_HEATER_B4S2" , "O2 Sensor Heater Monitor Bank 4 - Sensor 2" , b"064E", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_HEATER_B4S3" , "O2 Sensor Heater Monitor Bank 4 - Sensor 3" , b"064F", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_O2_HEATER_B4S4" , "O2 Sensor Heater Monitor Bank 4 - Sensor 4" , b"0650", 0, monitor, ECU.ALL, False),
+] + ([None] * 15) + [ # 51 - 5F Reserved
+ OBDCommand("MIDS_D" , "Supported MIDs [61-80]" , b"0660", 0, pid, ECU.ALL, False),
+ OBDCommand("MONITOR_HEATED_CATALYST_B1" , "Heated Catalyst Monitor Bank 1" , b"0661", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_HEATED_CATALYST_B2" , "Heated Catalyst Monitor Bank 2" , b"0662", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_HEATED_CATALYST_B3" , "Heated Catalyst Monitor Bank 3" , b"0663", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_HEATED_CATALYST_B4" , "Heated Catalyst Monitor Bank 4" , b"0664", 0, monitor, ECU.ALL, False),
+] + ([None] * 12) + [ # 65 - 70 Reserved
+ OBDCommand("MONITOR_SECONDARY_AIR_1" , "Secondary Air Monitor 1" , b"0671", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_SECONDARY_AIR_2" , "Secondary Air Monitor 2" , b"0672", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_SECONDARY_AIR_3" , "Secondary Air Monitor 3" , b"0673", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_SECONDARY_AIR_4" , "Secondary Air Monitor 4" , b"0674", 0, monitor, ECU.ALL, False),
+] + ([None] * 11) + [ # 75 - 7F Reserved
+ OBDCommand("MIDS_E" , "Supported MIDs [81-A0]" , b"0680", 0, pid, ECU.ALL, False),
+ OBDCommand("MONITOR_FUEL_SYSTEM_B1" , "Fuel System Monitor Bank 1" , b"0681", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_FUEL_SYSTEM_B2" , "Fuel System Monitor Bank 2" , b"0682", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_FUEL_SYSTEM_B3" , "Fuel System Monitor Bank 3" , b"0683", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_FUEL_SYSTEM_B4" , "Fuel System Monitor Bank 4" , b"0684", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_BOOST_PRESSURE_B1" , "Boost Pressure Control Monitor Bank 1" , b"0685", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_BOOST_PRESSURE_B2" , "Boost Pressure Control Monitor Bank 1" , b"0686", 0, monitor, ECU.ALL, False),
+] + ([None] * 9) + [ # 87 - 8F Reserved
+ OBDCommand("MONITOR_NOX_ABSORBER_B1" , "NOx Absorber Monitor Bank 1" , b"0690", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_NOX_ABSORBER_B2" , "NOx Absorber Monitor Bank 2" , b"0691", 0, monitor, ECU.ALL, False),
+] + ([None] * 6) + [ # 92 - 97 Reserved
+ OBDCommand("MONITOR_NOX_CATALYST_B1" , "NOx Catalyst Monitor Bank 1" , b"0698", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_NOX_CATALYST_B2" , "NOx Catalyst Monitor Bank 2" , b"0699", 0, monitor, ECU.ALL, False),
+] + ([None] * 6) + [ # 9A - 9F Reserved
+ OBDCommand("MIDS_F" , "Supported MIDs [A1-C0]" , b"06A0", 0, pid, ECU.ALL, False),
+ OBDCommand("MONITOR_MISFIRE_GENERAL" , "Misfire Monitor General Data" , b"06A1", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_MISFIRE_CYLINDER_1" , "Misfire Cylinder 1 Data" , b"06A2", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_MISFIRE_CYLINDER_2" , "Misfire Cylinder 2 Data" , b"06A3", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_MISFIRE_CYLINDER_3" , "Misfire Cylinder 3 Data" , b"06A4", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_MISFIRE_CYLINDER_4" , "Misfire Cylinder 4 Data" , b"06A5", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_MISFIRE_CYLINDER_5" , "Misfire Cylinder 5 Data" , b"06A6", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_MISFIRE_CYLINDER_6" , "Misfire Cylinder 6 Data" , b"06A7", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_MISFIRE_CYLINDER_7" , "Misfire Cylinder 7 Data" , b"06A8", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_MISFIRE_CYLINDER_8" , "Misfire Cylinder 8 Data" , b"06A9", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_MISFIRE_CYLINDER_9" , "Misfire Cylinder 9 Data" , b"06AA", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_MISFIRE_CYLINDER_10" , "Misfire Cylinder 10 Data" , b"06AB", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_MISFIRE_CYLINDER_11" , "Misfire Cylinder 11 Data" , b"06AC", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_MISFIRE_CYLINDER_12" , "Misfire Cylinder 12 Data" , b"06AD", 0, monitor, ECU.ALL, False),
+] + ([None] * 2) + [ # AE - AF Reserved
+ OBDCommand("MONITOR_PM_FILTER_B1" , "PM Filter Monitor Bank 1" , b"06B0", 0, monitor, ECU.ALL, False),
+ OBDCommand("MONITOR_PM_FILTER_B2" , "PM Filter Monitor Bank 2" , b"06B1", 0, monitor, ECU.ALL, False),
]
__mode7__ = [
+ # name description cmd bytes decoder ECU fast
+ OBDCommand("GET_CURRENT_DTC" , "Get DTCs from the current/last driving cycle" , b"07", 0, dtc, ECU.ALL, False),
+]
+
+__mode9__ = [
# name description cmd bytes decoder ECU fast
- OBDCommand("GET_FREEZE_DTC" , "Get Freeze DTCs" , "07", 0, dtc, ECU.ALL, False),
+ # OBDCommand("PIDS_9A" , "Supported PIDs [01-20]" , b"0900", 4, pid, ECU.ENGINE, True),
+ # OBDCommand("VIN_MESSAGE_COUNT" , "VIN Message Count" , b"0901", 1, uas(0x01), ECU.ENGINE, True),
+ # OBDCommand("VIN" , "Get Vehicle Identification Number" , b"0902", 20, raw_string, ECU.ENGINE, True),
]
__misc__ = [
# name description cmd bytes decoder ECU fast
- OBDCommand("ELM_VERSION" , "ELM327 version string" , "ATI", 0, raw_string, ECU.UNKNOWN, False),
- OBDCommand("ELM_VOLTAGE" , "Voltage detected by OBD-II adapter" , "ATRV", 0, elm_voltage, ECU.UNKNOWN, False),
+ OBDCommand("ELM_VERSION" , "ELM327 version string" , b"ATI", 0, raw_string, ECU.UNKNOWN, False),
+ OBDCommand("ELM_VOLTAGE" , "Voltage detected by OBD-II adapter" , b"ATRV", 0, elm_voltage, ECU.UNKNOWN, False),
]
@@ -199,14 +314,17 @@ def __init__(self):
__mode3__,
__mode4__,
[],
+ __mode6__,
+ __mode7__,
[],
- __mode7__
+ __mode9__,
]
# allow commands to be accessed by name
for m in self.modes:
for c in m:
- self.__dict__[c.name] = c
+ if c is not None:
+ self.__dict__[c.name] = c
for c in __misc__:
self.__dict__[c.name] = c
@@ -226,20 +344,17 @@ def __getitem__(self, key):
elif isinstance(key, str) or isinstance(key, unicode):
return self.__dict__[key]
else:
- debug("OBD commands can only be retrieved by PID value or dict name", True)
+ logger.warning("OBD commands can only be retrieved by PID value or dict name")
def __len__(self):
""" returns the number of commands supported by python-OBD """
- l = 0
- for m in self.modes:
- l += len(m)
- return l
+ return sum([len(mode) for mode in self.modes])
- def __contains__(self, s):
+ def __contains__(self, name):
""" calls has_name(s) """
- return self.has_name(s)
+ return self.has_name(name)
def base_commands(self):
@@ -249,9 +364,10 @@ def base_commands(self):
"""
return [
self.PIDS_A,
+ self.MIDS_A,
self.GET_DTC,
self.CLEAR_DTC,
- self.GET_FREEZE_DTC,
+ self.GET_CURRENT_DTC,
self.ELM_VERSION,
self.ELM_VOLTAGE,
]
@@ -260,53 +376,33 @@ def base_commands(self):
def pid_getters(self):
""" returns a list of PID GET commands """
getters = []
- for m in self.modes:
- for c in m:
- if c.decode == pid: # GET commands have a special decoder
- getters.append(c)
+ for mode in self.modes:
+ getters += [ cmd for cmd in mode if (cmd and cmd.decode == pid) ]
return getters
- def set_supported(self, mode, pid, v):
- """ sets the boolean supported flag for the given command """
- if isinstance(v, bool):
- if self.has(mode, pid):
- self.modes[mode][pid].supported = v
- else:
- debug("set_supported() only accepts boolean values", True)
-
-
def has_command(self, c):
""" checks for existance of a command by OBDCommand object """
- if isinstance(c, OBDCommand):
- return c in self.__dict__.values()
- else:
- debug("has_command() only accepts OBDCommand objects", True)
- return False
+ return c in self.__dict__.values()
- def has_name(self, s):
+ def has_name(self, name):
""" checks for existance of a command by name """
- if isinstance(s, str) or isinstance(s, unicode):
- return s.isupper() and (s in self.__dict__.keys())
- else:
- debug("has_name() only accepts string names for commands", True)
- return False
+ # isupper() rejects all the normal properties
+ return name.isupper() and name in self.__dict__
def has_pid(self, mode, pid):
""" checks for existance of a command by int mode and int pid """
- if isinstance(mode, int) and isinstance(pid, int):
- if (mode < 0) or (pid < 0):
- return False
- if mode >= len(self.modes):
- return False
- if pid >= len(self.modes[mode]):
- return False
- return True
- else:
- debug("has_pid() only accepts integer values for mode and PID", True)
+ if (mode < 0) or (pid < 0):
+ return False
+ if mode >= len(self.modes):
return False
+ if pid >= len(self.modes[mode]):
+ return False
+
+ # make sure that the command isn't reserved
+ return (self.modes[mode][pid] is not None)
# export this object
diff --git a/obd/debug.py b/obd/debug.py
deleted file mode 100644
index 06105697..00000000
--- a/obd/debug.py
+++ /dev/null
@@ -1,50 +0,0 @@
-
-########################################################################
-# #
-# python-OBD: A python OBD-II serial module derived from pyobd #
-# #
-# Copyright 2004 Donour Sizemore (donour@uchicago.edu) #
-# Copyright 2009 Secons Ltd. (www.obdtester.com) #
-# Copyright 2009 Peter J. Creath #
-# Copyright 2016 Brendan Whitfield (brendan-w.com) #
-# #
-########################################################################
-# #
-# debug.py #
-# #
-# This file is part of python-OBD (a derivative of pyOBD) #
-# #
-# python-OBD is free software: you can redistribute it and/or modify #
-# it under the terms of the GNU General Public License as published by #
-# the Free Software Foundation, either version 2 of the License, or #
-# (at your option) any later version. #
-# #
-# python-OBD is distributed in the hope that it will be useful, #
-# but WITHOUT ANY WARRANTY; without even the implied warranty of #
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
-# GNU General Public License for more details. #
-# #
-# You should have received a copy of the GNU General Public License #
-# along with python-OBD. If not, see . #
-# #
-########################################################################
-
-class Debug():
- def __init__(self):
- self.console = False
- self.handler = None
-
- def __call__(self, msg, forcePrint=False):
-
- if self.console or forcePrint:
- print("[obd] " + str(msg))
-
- if hasattr(self.handler, '__call__'):
- self.handler(msg)
-
-debug = Debug()
-
-
-class ProtocolError(Exception):
- def __init__(self):
- pass
diff --git a/obd/decoders.py b/obd/decoders.py
index f6767b1a..ce7b31c8 100644
--- a/obd/decoders.py
+++ b/obd/decoders.py
@@ -30,10 +30,15 @@
########################################################################
import math
+import functools
from .utils import *
from .codes import *
-from .debug import debug
-from .OBDResponse import Unit, Status, Test
+from .OBDResponse import Status, StatusTest, Monitor, MonitorTest
+from .UnitsAndScaling import Unit, UAS_IDS
+
+import logging
+
+logger = logging.getLogger(__name__)
'''
All decoders take the form:
@@ -45,200 +50,184 @@ def ():
'''
+
# drop all messages, return None
def drop(messages):
- return (None, Unit.NONE)
+ return None
# data in, data out
def noop(messages):
- return (messages[0].data, Unit.NONE)
+ return messages[0].data
# hex in, bitstring out
def pid(messages):
d = messages[0].data
- v = bytes_to_bits(d)
- return (v, Unit.NONE)
+ return bitarray(d)
# returns the raw strings from the ELM
def raw_string(messages):
- return ("\n".join([m.raw() for m in messages]), Unit.NONE)
+ return "\n".join([m.raw() for m in messages])
-'''
-Sensor decoders
-Return Value object with value and units
-'''
-def count(messages):
+"""
+Some decoders are simple and are already implemented in the Units And Scaling
+tables (used mainly for Mode 06). The uas() decoder is a wrapper for any
+Unit/Scaling in that table, simply to avoid redundant code.
+"""
+
+def uas(id):
+ """ get the corresponding decoder for this UAS ID """
+ return functools.partial(decode_uas, id=id)
+
+def decode_uas(messages, id):
d = messages[0].data
- v = bytes_to_int(d)
- return (v, Unit.COUNT)
+ return UAS_IDS[id](d)
+
+
+"""
+General sensor decoders
+Return pint Quantities
+"""
# 0 to 100 %
def percent(messages):
d = messages[0].data
v = d[0]
v = v * 100.0 / 255.0
- return (v, Unit.PERCENT)
+ return v * Unit.percent
# -100 to 100 %
def percent_centered(messages):
d = messages[0].data
v = d[0]
v = (v - 128) * 100.0 / 128.0
- return (v, Unit.PERCENT)
+ return v * Unit.percent
# -40 to 215 C
def temp(messages):
d = messages[0].data
v = bytes_to_int(d)
v = v - 40
- return (v, Unit.C)
-
-# -40 to 6513.5 C
-def catalyst_temp(messages):
- d = messages[0].data
- v = bytes_to_int(d)
- v = (v / 10.0) - 40
- return (v, Unit.C)
+ return Unit.Quantity(v, Unit.celsius) # non-multiplicative unit
# -128 to 128 mA
def current_centered(messages):
d = messages[0].data
v = bytes_to_int(d[2:4])
v = (v / 256.0) - 128
- return (v, Unit.MA)
+ return v * Unit.milliampere
# 0 to 1.275 volts
def sensor_voltage(messages):
d = messages[0].data
- v = d[0]
- v = v / 200.0
- return (v, Unit.VOLT)
+ v = d[0] / 200.0
+ return v * Unit.volt
# 0 to 8 volts
def sensor_voltage_big(messages):
d = messages[0].data
v = bytes_to_int(d[2:4])
v = (v * 8.0) / 65535
- return (v, Unit.VOLT)
+ return v * Unit.volt
# 0 to 765 kPa
def fuel_pressure(messages):
d = messages[0].data
v = d[0]
v = v * 3
- return (v, Unit.KPA)
+ return v * Unit.kilopascal
# 0 to 255 kPa
def pressure(messages):
d = messages[0].data
v = d[0]
- return (v, Unit.KPA)
-
-# 0 to 5177 kPa
-def fuel_pres_vac(messages):
- d = messages[0].data
- v = bytes_to_int(d)
- v = v * 0.079
- return (v, Unit.KPA)
-
-# 0 to 655,350 kPa
-def fuel_pres_direct(messages):
- d = messages[0].data
- v = bytes_to_int(d)
- v = v * 10
- return (v, Unit.KPA)
+ return v * Unit.kilopascal
# -8192 to 8192 Pa
def evap_pressure(messages):
# decode the twos complement
d = messages[0].data
- a = twos_comp(unhex(d[0]), 8)
- b = twos_comp(unhex(d[1]), 8)
+ a = twos_comp(d[0], 8)
+ b = twos_comp(d[1], 8)
v = ((a * 256.0) + b) / 4.0
- return (v, Unit.PA)
+ return v * Unit.pascal
# 0 to 327.675 kPa
def abs_evap_pressure(messages):
d = messages[0].data
v = bytes_to_int(d)
v = v / 200.0
- return (v, Unit.KPA)
+ return v * Unit.kilopascal
# -32767 to 32768 Pa
def evap_pressure_alt(messages):
d = messages[0].data
v = bytes_to_int(d)
v = v - 32767
- return (v, Unit.PA)
-
-# 0 to 16,383.75 RPM
-def rpm(messages):
- d = messages[0].data
- v = bytes_to_int(d) / 4.0
- return (v, Unit.RPM)
-
-# 0 to 255 KPH
-def speed(messages):
- d = messages[0].data
- v = bytes_to_int(d)
- return (v, Unit.KPH)
+ return v * Unit.pascal
# -64 to 63.5 degrees
def timing_advance(messages):
d = messages[0].data
v = d[0]
v = (v - 128) / 2.0
- return (v, Unit.DEGREES)
+ return v * Unit.degree
# -210 to 301 degrees
def inject_timing(messages):
d = messages[0].data
v = bytes_to_int(d)
v = (v - 26880) / 128.0
- return (v, Unit.DEGREES)
-
-# 0 to 655.35 grams/sec
-def maf(messages):
- d = messages[0].data
- v = bytes_to_int(d)
- v = v / 100.0
- return (v, Unit.GPS)
+ return v * Unit.degree
# 0 to 2550 grams/sec
def max_maf(messages):
d = messages[0].data
v = d[0]
v = v * 10
- return (v, Unit.GPS)
+ return v * Unit.gps
-# 0 to 65535 seconds
-def seconds(messages):
+# 0 to 3212 Liters/hour
+def fuel_rate(messages):
d = messages[0].data
v = bytes_to_int(d)
- return (v, Unit.SEC)
+ v = v * 0.05
+ return v * Unit.liters_per_hour
-# 0 to 65535 minutes
-def minutes(messages):
+# special bit encoding for PID 13
+def o2_sensors(messages):
d = messages[0].data
- v = bytes_to_int(d)
- return (v, Unit.MIN)
-
-# 0 to 65535 km
-def distance(messages):
+ bits = bitarray(d)
+ return (
+ (), # bank 0 is invalid
+ tuple(bits[:4]), # bank 1
+ tuple(bits[4:]), # bank 2
+ )
+
+def aux_input_status(messages):
d = messages[0].data
- v = bytes_to_int(d)
- return (v, Unit.KM)
+ return ((d[0] >> 7) & 1) == 1 # first bit indicate PTO status
-# 0 to 3212 Liters/hour
-def fuel_rate(messages):
+# special bit encoding for PID 1D
+def o2_sensors_alt(messages):
+ d = messages[0].data
+ bits = bitarray(d)
+ return (
+ (), # bank 0 is invalid
+ tuple(bits[:2]), # bank 1
+ tuple(bits[2:4]), # bank 2
+ tuple(bits[4:6]), # bank 3
+ tuple(bits[6:]), # bank 4
+ )
+
+# 0 to 25700 %
+def absolute_load(messages):
d = messages[0].data
v = bytes_to_int(d)
- v = v * 0.05
- return (v, Unit.LPH)
-
+ v *= 100.0 / 255.0
+ return v * Unit.percent
def elm_voltage(messages):
# doesn't register as a normal OBD response,
@@ -246,10 +235,10 @@ def elm_voltage(messages):
v = messages[0].frames[0].raw
try:
- return (float(v), Unit.VOLT)
+ return float(v) * Unit.volt
except ValueError:
- debug("Failed to parse ELM voltage", True)
- return (None, Unit.NONE)
+ logger.warning("Failed to parse ELM voltage")
+ return None
'''
@@ -261,95 +250,80 @@ def elm_voltage(messages):
def status(messages):
d = messages[0].data
- bits = bytes_to_bits(d)
+ bits = bitarray(d)
+
+ # ┌Components not ready
+ # |┌Fuel not ready
+ # ||┌Misfire not ready
+ # |||┌Spark vs. Compression
+ # ||||┌Components supported
+ # |||||┌Fuel supported
+ # ┌MIL ||||||┌Misfire supported
+ # | |||||||
+ # 10000011 00000111 11111111 00000000
+ # [# DTC] X [supprt] [~ready]
output = Status()
- output.MIL = bitToBool(bits[0])
- output.DTC_count = unbin(bits[1:8])
- output.ignition_type = IGNITION_TYPE[unbin(bits[12])]
-
- output.tests.append(Test("Misfire", \
- bitToBool(bits[15]), \
- bitToBool(bits[11])))
-
- output.tests.append(Test("Fuel System", \
- bitToBool(bits[14]), \
- bitToBool(bits[10])))
-
- output.tests.append(Test("Components", \
- bitToBool(bits[13]), \
- bitToBool(bits[9])))
-
-
- # different tests for different ignition types
- if(output.ignition_type == IGNITION_TYPE[0]): # spark
- for i in range(8):
- if SPARK_TESTS[i] is not None:
+ output.MIL = bits[0]
+ output.DTC_count = bits.value(1, 8)
+ output.ignition_type = IGNITION_TYPE[int(bits[12])]
- t = Test(SPARK_TESTS[i], \
- bitToBool(bits[(2 * 8) + i]), \
- bitToBool(bits[(3 * 8) + i]))
+ # load the 3 base tests that are always present
+ for i, name in enumerate(BASE_TESTS[::-1]):
+ t = StatusTest(name, bits[13 + i], not bits[9 + i])
+ output.__dict__[name] = t
- output.tests.append(t)
+ # different tests for different ignition types
+ if bits[12]: # compression
+ for i, name in enumerate(COMPRESSION_TESTS[::-1]): # reverse to correct for bit vs. indexing order
+ t = StatusTest(name, bits[(2 * 8) + i],
+ not bits[(3 * 8) + i])
+ output.__dict__[name] = t
- elif(output.ignition_type == IGNITION_TYPE[1]): # compression
- for i in range(8):
- if COMPRESSION_TESTS[i] is not None:
+ else: # spark
+ for i, name in enumerate(SPARK_TESTS[::-1]): # reverse to correct for bit vs. indexing order
+ t = StatusTest(name, bits[(2 * 8) + i],
+ not bits[(3 * 8) + i])
+ output.__dict__[name] = t
- t = Test(COMPRESSION_TESTS[i], \
- bitToBool(bits[(2 * 8) + i]), \
- bitToBool(bits[(3 * 8) + i]))
-
- output.tests.append(t)
-
- return (output, Unit.NONE)
+ return output
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)
- return (None, Unit.NONE)
-
- i = math.log(v, 2) # only a single bit should be on
+ bits = bitarray(d)
- if i % 1 != 0:
- debug("Invalid fuel status response (multiple bits set)", True)
- return (None, Unit.NONE)
+ status_1 = ""
+ status_2 = ""
- i = int(i)
+ if bits[0:8].count(True) == 1:
+ status_1 = FUEL_STATUS[ 7 - bits[0:8].index(True) ]
+ else:
+ logger.debug("Invalid response for fuel status (multiple/no bits set)")
- if i >= len(FUEL_STATUS):
- debug("Invalid fuel status response (no table entry)", True)
- return (None, Unit.NONE)
+ if bits[8:16].count(True) == 1:
+ status_2 = FUEL_STATUS[ 7 - bits[8:16].index(True) ]
+ else:
+ logger.debug("Invalid response for fuel status (multiple/no bits set)")
- return (FUEL_STATUS[i], Unit.NONE)
+ if not status_1 and not status_2:
+ return None
+ else:
+ return (status_1, status_2)
def air_status(messages):
d = messages[0].data
- v = d[0]
-
- if v <= 0:
- debug("Invalid air status response (v <= 0)", True)
- return (None, Unit.NONE)
-
- i = math.log(v, 2) # only a single bit should be on
-
- if i % 1 != 0:
- debug("Invalid air status response (multiple bits set)", True)
- return (None, Unit.NONE)
-
- i = int(i)
+ bits = bitarray(d)
- if i >= len(AIR_STATUS):
- debug("Invalid air status response (no table entry)", True)
- return (None, Unit.NONE)
+ status = None
+ if bits.num_set() == 1:
+ status = AIR_STATUS[ 7 - bits[0:8].index(True) ]
+ else:
+ logger.debug("Invalid response for fuel status (multiple/no bits set)")
- return (AIR_STATUS[i], Unit.NONE)
+ return status
def obd_compliance(_hex):
@@ -361,7 +335,7 @@ def obd_compliance(_hex):
if i < len(OBD_COMPLIANCE):
v = OBD_COMPLIANCE[i]
- return (v, Unit.NONE)
+ return v
def fuel_type(_hex):
@@ -373,10 +347,10 @@ def fuel_type(_hex):
if i < len(FUEL_TYPES):
v = FUEL_TYPES[i]
- return (v, Unit.NONE)
+ return v
-def single_dtc(_bytes):
+def parse_dtc(_bytes):
""" converts 2 bytes into a DTC code """
# check validity (also ignores padding that the ELM returns)
@@ -394,7 +368,14 @@ def single_dtc(_bytes):
dtc += str( (_bytes[0] >> 4) & 0b0011 ) # the next pair of 2 bits. Mask off the bits we read above
dtc += bytes_to_hex(_bytes)[1:4]
- return dtc
+ # pull a description if we have one
+ return (dtc, DTC.get(dtc, ""))
+
+
+def single_dtc(messages):
+ """ parses a single DTC from a message """
+ d = messages[0].data
+ return parse_dtc(d)
def dtc(messages):
@@ -409,15 +390,59 @@ def dtc(messages):
for n in range(1, len(d), 2):
# parse the code
- dtc = single_dtc( (d[n-1], d[n]) )
+ dtc = parse_dtc( (d[n-1], d[n]) )
if dtc is not None:
- # pull a description if we have one
- if dtc in DTC:
- desc = DTC[dtc]
- else:
- desc = "Unknown error code"
+ codes.append(dtc)
+
+ return codes
+
+
+def parse_monitor_test(d, mon):
+ test = MonitorTest()
+
+ tid = d[1]
+
+ if tid in TEST_IDS:
+ test.name = TEST_IDS[tid][0] # lookup the name from the table
+ test.desc = TEST_IDS[tid][1] # lookup the description from the table
+ else:
+ logger.debug("Encountered unknown Test ID")
+ test.name = "Unknown"
+ test.desc = "Unknown"
+
+ uas = UAS_IDS.get(d[2], None)
+
+ # if we can't decode the value, abort
+ if uas is None:
+ logger.debug("Encountered unknown Units and Scaling ID")
+ return None
+
+ # load the test results
+ test.tid = tid
+ test.value = uas(d[3:5]) # convert bytes to actual values
+ test.min = uas(d[5:7])
+ test.max = uas(d[7:])
+
+ return test
+
+
+def monitor(messages):
+ d = messages[0].data
+ mon = Monitor()
+
+ # test that we got the right number of bytes
+ extra_bytes = len(d) % 9
+
+ if extra_bytes != 0:
+ logger.debug("Encountered monitor message with non-multiple of 9 bytes. Truncating...")
+ d = d[:len(d) - extra_bytes]
- codes.append( (dtc, desc) )
+ # look at data in blocks of 9 bytes (one test result)
+ for n in range(0, len(d), 9):
+ # extract the 9 byte block, and parse a new MonitorTest
+ test = parse_monitor_test(d[n:n + 9], mon)
+ if test is not None:
+ mon.add_test(test)
- return (codes, Unit.NONE)
+ return mon
diff --git a/obd/elm327.py b/obd/elm327.py
index 0b0841f9..f4175eea 100644
--- a/obd/elm327.py
+++ b/obd/elm327.py
@@ -32,10 +32,11 @@
import re
import serial
import time
+import logging
from .protocols import *
from .utils import OBDStatus
-from .debug import debug
+logger = logging.getLogger(__name__)
class ELM327:
@@ -53,6 +54,8 @@ class ELM327:
ecus()
"""
+ ELM_PROMPT = b'>'
+
_SUPPORTED_PROTOCOLS = {
#"0" : None, # Automatic Mode. This isn't an actual protocol. If the
# ELM reports this, then we don't have enough
@@ -85,10 +88,30 @@ class ELM327:
"A", # SAE_J1939
]
+ # 38400, 9600 are the possible boot bauds (unless reprogrammed via
+ # PP 0C). 19200, 38400, 57600, 115200, 230400, 500000 are listed on
+ # p.46 of the ELM327 datasheet.
+ #
+ # Once pyserial supports non-standard baud rates on platforms other
+ # than Linux, we'll add 500K to this list.
+ #
+ # We check the two default baud rates first, then go fastest to
+ # slowest, on the theory that anyone who's using a slow baud rate is
+ # going to be less picky about the time required to detect it.
+ _TRY_BAUDS = [ 38400, 9600, 230400, 115200, 57600, 19200 ]
+
+
def __init__(self, portname, baudrate, protocol):
"""Initializes port by resetting device and gettings supported PIDs. """
+ logger.info("Initializing ELM327: PORT=%s BAUD=%s PROTOCOL=%s" %
+ (
+ portname,
+ "auto" if baudrate is None else baudrate,
+ "auto" if protocol is None else protocol,
+ ))
+
self.__status = OBDStatus.NOT_CONNECTED
self.__port = None
self.__protocol = UnknownProtocol([])
@@ -96,15 +119,11 @@ def __init__(self, portname, baudrate, protocol):
# ------------- open port -------------
try:
- debug("Opening serial port '%s'" % portname)
self.__port = serial.Serial(portname, \
- baudrate = baudrate, \
parity = serial.PARITY_NONE, \
stopbits = 1, \
- bytesize = 8, \
- timeout = 3) # seconds
- debug("Serial port successfully opened on " + self.port_name())
-
+ bytesize = 8,
+ timeout = 10) # seconds
except serial.SerialException as e:
self.__error(e)
return
@@ -112,29 +131,34 @@ def __init__(self, portname, baudrate, protocol):
self.__error(e)
return
+ # ------------------------ find the ELM's baud ------------------------
+
+ if not self.set_baudrate(baudrate):
+ self.__error("Failed to set baudrate")
+ return
# ---------------------------- ATZ (reset) ----------------------------
try:
- self.__send("ATZ", delay=1) # wait 1 second for ELM to initialize
+ self.__send(b"ATZ", delay=1) # wait 1 second for ELM to initialize
# return data can be junk, so don't bother checking
except serial.SerialException as e:
self.__error(e)
return
# -------------------------- ATE0 (echo OFF) --------------------------
- r = self.__send("ATE0")
+ r = self.__send(b"ATE0")
if not self.__isok(r, expectEcho=True):
self.__error("ATE0 did not return 'OK'")
return
# ------------------------- ATH1 (headers ON) -------------------------
- r = self.__send("ATH1")
+ r = self.__send(b"ATH1")
if not self.__isok(r):
self.__error("ATH1 did not return 'OK', or echoing is still ON")
return
# ------------------------ ATL0 (linefeeds OFF) -----------------------
- r = self.__send("ATL0")
+ r = self.__send(b"ATL0")
if not self.__isok(r):
self.__error("ATL0 did not return 'OK'")
return
@@ -143,18 +167,23 @@ def __init__(self, portname, baudrate, protocol):
self.__status = OBDStatus.ELM_CONNECTED
# try to communicate with the car, and load the correct protocol parser
- if self.load_protocol(protocol):
+ if self.set_protocol(protocol):
self.__status = OBDStatus.CAR_CONNECTED
- debug("Connection successful")
+ logger.info("Connected Successfully: PORT=%s BAUD=%s PROTOCOL=%s" %
+ (
+ portname,
+ self.__port.baudrate,
+ self.__protocol.ELM_ID,
+ ))
else:
- debug("Connected to the adapter, but failed to connect to the vehicle", True)
+ logger.error("Connected to the adapter, but failed to connect to the vehicle")
- def load_protocol(self, protocol):
+ def set_protocol(self, protocol):
if protocol is not None:
# an explicit protocol was specified
if protocol not in self._SUPPORTED_PROTOCOLS:
- debug("%s is not a valid protocol. Please use \"1\" through \"A\"", True)
+ logger.error("%s is not a valid protocol. Please use \"1\" through \"A\"")
return False
return self.manual_protocol(protocol)
else:
@@ -163,9 +192,8 @@ def load_protocol(self, protocol):
def manual_protocol(self, protocol):
-
- r = self.__send("ATTP%s" % protocol)
- r0100 = self.__send("0100")
+ r = self.__send(b"ATTP" + protocol.encode())
+ r0100 = self.__send(b"0100")
if not self.__has_message(r0100, "UNABLE TO CONNECT"):
# success, found the protocol
@@ -186,15 +214,15 @@ def auto_protocol(self):
"""
# -------------- try the ELM's auto protocol mode --------------
- r = self.__send("ATSP0")
+ r = self.__send(b"ATSP0")
# -------------- 0100 (first command, SEARCH protocols) --------------
- r0100 = self.__send("0100")
+ r0100 = self.__send(b"0100")
# ------------------- ATDPN (list protocol number) -------------------
- r = self.__send("ATDPN")
+ r = self.__send(b"ATDPN")
if len(r) != 1:
- debug("Failed to retrieve current protocol", True)
+ logger.error("Failed to retrieve current protocol")
return False
@@ -211,17 +239,68 @@ def auto_protocol(self):
# an unknown protocol
# this is likely because not all adapter/car combinations work
# in "auto" mode. Some respond to ATDPN responded with "0"
- debug("ELM responded with unknown protocol. Trying them one-by-one")
+ logger.debug("ELM responded with unknown protocol. Trying them one-by-one")
for p in self._TRY_PROTOCOL_ORDER:
- r = self.__send("ATTP%s" % p)
- r0100 = self.__send("0100")
+ r = self.__send(b"ATTP" + p.encode())
+ r0100 = self.__send(b"0100")
if not self.__has_message(r0100, "UNABLE TO CONNECT"):
# success, found the protocol
self.__protocol = self._SUPPORTED_PROTOCOLS[p](r0100)
return True
# if we've come this far, then we have failed...
+ logger.error("Failed to determine protocol")
+ return False
+
+
+ def set_baudrate(self, baud):
+ if baud is None:
+ # when connecting to pseudo terminal, don't bother with auto baud
+ if self.port_name().startswith("/dev/pts"):
+ logger.debug("Detected pseudo terminal, skipping baudrate setup")
+ return True
+ else:
+ return self.auto_baudrate()
+ else:
+ self.__port.baudrate = baud
+ return True
+
+
+ def auto_baudrate(self):
+ """
+ Detect the baud rate at which a connected ELM32x interface is operating.
+ Returns boolean for success.
+ """
+
+ # before we change the timout, save the "normal" value
+ timeout = self.__port.timeout
+ self.__port.timeout = 0.1 # we're only talking with the ELM, so things should go quickly
+
+ for baud in self._TRY_BAUDS:
+ self.__port.baudrate = baud
+ self.__port.flushInput()
+ self.__port.flushOutput()
+
+ # Send a nonsense command to get a prompt back from the scanner
+ # (an empty command runs the risk of repeating a dangerous command)
+ # The first character might get eaten if the interface was busy,
+ # so write a second one (again so that the lone CR doesn't repeat
+ # the previous command)
+ self.__port.write(b"\x7F\x7F\r\n")
+ self.__port.flush()
+ response = self.__port.read(1024)
+ logger.debug("Response from baud %d: %s" % (baud, repr(response)))
+
+ # watch for the prompt character
+ if response.endswith(b">"):
+ logger.debug("Choosing baud %d" % baud)
+ self.__port.timeout = timeout # reinstate our original timeout
+ return True
+
+
+ logger.debug("Failed to choose baud")
+ self.__port.timeout = timeout # reinstate our original timeout
return False
@@ -244,21 +323,17 @@ def __has_message(self, lines, text):
return False
- def __error(self, msg=None):
- """ handles fatal failures, print debug info and closes serial """
-
+ def __error(self, msg):
+ """ handles fatal failures, print logger.info info and closes serial """
self.close()
-
- debug("Connection Error:", True)
- if msg is not None:
- debug(' ' + str(msg), True)
+ logger.error(str(msg))
def port_name(self):
if self.__port is not None:
return self.__port.portstr
else:
- return "No Port"
+ return ""
def status(self):
@@ -287,7 +362,8 @@ def close(self):
self.__protocol = None
if self.__port is not None:
- self.__write("ATZ")
+ logger.info("closing port")
+ self.__write(b"ATZ")
self.__port.close()
self.__port = None
@@ -305,7 +381,7 @@ def send_and_parse(self, cmd):
"""
if self.__status == OBDStatus.NOT_CONNECTED:
- debug("cannot send_and_parse() when unconnected", True)
+ logger.info("cannot send_and_parse() when unconnected")
return None
lines = self.__send(cmd)
@@ -325,7 +401,7 @@ def __send(self, cmd, delay=None):
self.__write(cmd)
if delay is not None:
- debug("wait: %d seconds" % delay)
+ logger.debug("wait: %d seconds" % delay)
time.sleep(delay)
return self.__read()
@@ -337,13 +413,13 @@ def __write(self, cmd):
"""
if self.__port:
- cmd += "\r\n" # terminate
- debug("write: " + repr(cmd))
+ cmd += b"\r\n" # terminate
+ logger.debug("write: " + repr(cmd))
self.__port.flushInput() # dump everything in the input buffer
- self.__port.write(cmd.encode()) # turn the string into bytes and write
+ self.__port.write(cmd) # turn the string into bytes and write
self.__port.flush() # wait for the output buffer to finish transmitting
else:
- debug("cannot perform __write() when unconnected", True)
+ logger.info("cannot perform __write() when unconnected")
def __read(self):
@@ -353,46 +429,41 @@ def __read(self):
accumulates characters until the prompt character is seen
returns a list of [/r/n] delimited strings
"""
+ if not self.__port:
+ logger.info("cannot perform __read() when unconnected")
+ return []
- attempts = 2
- buffer = b''
+ buffer = bytearray()
- if self.__port:
- while True:
- c = self.__port.read(1)
+ while True:
+ # retrieve as much data as possible
+ data = self.__port.read(self.__port.in_waiting or 1)
- # if nothing was recieved
- if not c:
+ # if nothing was recieved
+ if not data:
+ logger.warning("Failed to read port")
+ break
- if attempts <= 0:
- debug("Failed to read port, giving up")
- break
+ buffer.extend(data)
- debug("Failed to read port, trying again...")
- attempts -= 1
- continue
+ # end on chevron (ELM prompt character)
+ if self.ELM_PROMPT in buffer:
+ break
- # end on chevron (ELM prompt character)
- if c == b'>':
- break
+ # log, and remove the "bytearray( ... )" part
+ logger.debug("read: " + repr(buffer)[10:-1])
- # skip null characters (ELM spec page 9)
- if c == b'\x00':
- continue
-
- buffer += c # whatever is left must be part of the response
- else:
- debug("cannot perform __read() when unconnected", True)
- return ""
+ # clean out any null characters
+ buffer = re.sub(b"\x00", b"", buffer)
- debug("read: " + repr(buffer))
+ # remove the prompt character
+ if buffer.endswith(self.ELM_PROMPT):
+ buffer = buffer[:-1]
# convert bytes into a standard string
- raw = buffer.decode()
+ string = buffer.decode()
- # splits into lines
- # removes empty lines
- # removes trailing spaces
- lines = [ s.strip() for s in re.split("[\r\n]", raw) if bool(s) ]
+ # splits into lines while removing empty lines and trailing spaces
+ lines = [ s.strip() for s in re.split("[\r\n]", string) if bool(s) ]
return lines
diff --git a/obd/obd.py b/obd/obd.py
index 14d22580..34899651 100644
--- a/obd/obd.py
+++ b/obd/obd.py
@@ -30,13 +30,15 @@
########################################################################
+import logging
+
from .__version__ import __version__
from .elm327 import ELM327
from .commands import commands
from .OBDResponse import OBDResponse
from .utils import scan_serial, OBDStatus
-from .debug import debug
+logger = logging.getLogger(__name__)
class OBD(object):
@@ -45,16 +47,16 @@ class OBD(object):
with it's assorted commands/sensors.
"""
- def __init__(self, portstr=None, baudrate=38400, protocol=None, fast=True):
- self.port = None
+ def __init__(self, portstr=None, baudrate=None, protocol=None, fast=True):
+ self.interface = None
self.supported_commands = set(commands.base_commands())
self.fast = fast
self.__last_command = "" # used for running the previous command with a CR
- debug("========================== python-OBD (v%s) ==========================" % __version__)
+ logger.info("======================= python-OBD (v%s) =======================" % __version__)
self.__connect(portstr, baudrate, protocol) # initialize by connecting and loading sensors
self.__load_commands() # try to load the car's supported commands
- debug("=========================================================================")
+ logger.info("===================================================================")
def __connect(self, portstr, baudrate, protocol):
@@ -63,26 +65,26 @@ def __connect(self, portstr, baudrate, protocol):
"""
if portstr is None:
- debug("Using scan_serial to select port")
+ logger.info("Using scan_serial to select port")
portnames = scan_serial()
- debug("Available ports: " + str(portnames))
+ logger.info("Available ports: " + str(portnames))
if not portnames:
- debug("No OBD-II adapters found", True)
+ logger.warning("No OBD-II adapters found")
return
for port in portnames:
- debug("Attempting to use port: " + str(port))
- self.port = ELM327(port, baudrate, protocol)
+ logger.info("Attempting to use port: " + str(port))
+ self.interface = ELM327(port, baudrate, protocol)
- if self.port.status() >= OBDStatus.ELM_CONNECTED:
+ if self.interface.status() >= OBDStatus.ELM_CONNECTED:
break # success! stop searching for serial
else:
- debug("Explicit port defined")
- self.port = ELM327(portstr, baudrate, protocol)
+ logger.info("Explicit port defined")
+ self.interface = ELM327(portstr, baudrate, protocol)
# if the connection failed, close it
- if self.port.status == OBDStatus.NOT_CONNECTED:
+ if self.interface.status() == OBDStatus.NOT_CONNECTED:
# the ELM327 class will report its own errors
self.close()
@@ -94,32 +96,31 @@ def __load_commands(self):
"""
if self.status() != OBDStatus.CAR_CONNECTED:
- debug("Cannot load commands: No connection to car", True)
+ logger.warning("Cannot load commands: No connection to car")
return
- debug("querying for supported PIDs (commands)...")
+ logger.info("querying for supported 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):
+ if not self.test_cmd(get, warn=False):
continue
# when querying, only use the blocking OBD.query()
# prevents problems when query is redefined in a subclass (like Async)
- response = OBD.query(self, get, force=True) # ask nicely
+ response = OBD.query(self, get)
if response.is_null():
+ logger.info("No valid data for PID listing command: %s" % get)
continue
- supported = response.value # string of binary 01010101010101
-
- # loop through PIDs binary
- for i in range(len(supported)):
- if supported[i] == "1":
+ # loop through PIDs bitarray
+ for i, bit in enumerate(response.value):
+ if bit:
- mode = get.mode_int
- pid = get.pid_int + i + 1
+ mode = get.mode
+ pid = get.pid + i + 1
if commands.has_pid(mode, pid):
self.supported_commands.add(commands[mode][pid])
@@ -128,7 +129,7 @@ def __load_commands(self):
if mode == 1 and commands.has_pid(2, pid):
self.supported_commands.add(commands[2][pid])
- debug("finished querying with %d commands supported" % len(self.supported_commands))
+ logger.info("finished querying with %d commands supported" % len(self.supported_commands))
def close(self):
@@ -136,60 +137,54 @@ def close(self):
Closes the connection, and clears supported_commands
"""
- self.supported_commands = []
+ self.supported_commands = set()
- if self.port is not None:
- debug("Closing connection")
- self.port.close()
- self.port = None
+ if self.interface is not None:
+ logger.info("Closing connection")
+ self.interface.close()
+ self.interface = None
def status(self):
""" returns the OBD connection status """
- if self.port is None:
+ if self.interface is None:
return OBDStatus.NOT_CONNECTED
else:
- return self.port.status()
+ return self.interface.status()
# not sure how useful this would be
# def ecus(self):
# """ returns a list of ECUs in the vehicle """
- # if self.port is None:
+ # if self.interface is None:
# return []
# else:
- # return self.port.ecus()
+ # return self.interface.ecus()
def protocol_name(self):
""" returns the name of the protocol being used by the ELM327 """
- if self.port is None:
+ if self.interface is None:
return ""
else:
- return self.port.protocol_name()
+ return self.interface.protocol_name()
def protocol_id(self):
""" returns the ID of the protocol being used by the ELM327 """
- if self.port is None:
+ if self.interface is None:
return ""
else:
- return self.port.protocol_id()
-
-
- 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()
+ return self.interface.protocol_id()
def port_name(self):
""" Returns the name of the currently connected port """
- if self.port is not None:
- return self.port.port_name()
+ if self.interface is not None:
+ return self.interface.port_name()
else:
- return "Not connected to any port"
+ return ""
def is_connected(self):
@@ -219,6 +214,26 @@ def supports(self, cmd):
return cmd in self.supported_commands
+ def test_cmd(self, cmd, warn=True):
+ """
+ Returns a boolean for whether a command will
+ be sent without using force=True.
+ """
+ # test if the command is supported
+ if not self.supports(cmd):
+ if warn:
+ logger.warning("'%s' is not supported" % str(cmd))
+ return False
+
+ # mode 06 is only implemented for the CAN protocols
+ if cmd.mode == 6 and self.interface.protocol_id() not in ["6", "7", "8", "9"]:
+ if warn:
+ logger.warning("Mode 06 commands are only supported over CAN protocols")
+ return False
+
+ return True
+
+
def query(self, cmd, force=False):
"""
primary API function. Sends commands to the car, and
@@ -226,25 +241,24 @@ def query(self, cmd, force=False):
"""
if self.status() == OBDStatus.NOT_CONNECTED:
- debug("Query failed, no connection available", True)
+ logger.warning("Query failed, no connection available")
return OBDResponse()
- if not force and not self.supports(cmd):
- debug("'%s' is not supported" % str(cmd), True)
+ # if the user forces, skip all checks
+ if not force and not self.test_cmd(cmd):
return OBDResponse()
-
# send command and retrieve message
- debug("Sending command: %s" % str(cmd))
+ logger.info("Sending command: %s" % str(cmd))
cmd_string = self.__build_command_string(cmd)
- messages = self.port.send_and_parse(cmd_string)
+ messages = self.interface.send_and_parse(cmd_string)
# if we're sending a new command, note it
if cmd_string:
self.__last_command = cmd_string
if not messages:
- debug("No valid OBD Messages returned", True)
+ logger.info("No valid OBD Messages returned")
return OBDResponse()
return cmd(messages) # compute a response object
@@ -256,10 +270,10 @@ def __build_command_string(self, cmd):
# only wait for as many ECUs as we've seen
if self.fast and cmd.fast:
- cmd_string += str(len(self.port.ecus()))
+ cmd_string += str(len(self.interface.ecus())).encode()
# if we sent this last time, just send
if self.fast and (cmd_string == self.__last_command):
- cmd_string = ""
+ cmd_string = b""
return cmd_string
diff --git a/obd/protocols/README.md b/obd/protocols/README.md
index 7f6a250f..e5b9c193 100644
--- a/obd/protocols/README.md
+++ b/obd/protocols/README.md
@@ -1,9 +1,9 @@
Notes
-----
-Each protocol object is callable, and accepts a list of raw input strings, and returns a list of parsed `Message` objects. The `data` field will contain a list of integers, corresponding to all relevant data returned by the command.
+Each protocol object is callable, and accepts a list of raw input strings, and returns a list of parsed `Message` objects. The `data` field will contain a bytearray, corresponding to all relevant data returned by the command.
-*Note: `Message.data` does not refer to the full data field of a message. Things like PCI/Mode/PID bytes are removed. If you want to see these fields, use `Frame.data` for the full (per-spec) data field.*
+*Note: `Message.data` does not refer to the full data field of a message. Things like PCI/Mode/PID bytes are often removed. If you want to see these fields, use `Frame.data` for the full (per-spec) data field.*
For example, these are the resultant `Message.data` fields for some single frame messages:
@@ -15,7 +15,7 @@ A CAN Message:
A J1850 Message:
48 6B 10 41 00 BE 7F B8 13 FF
[ data ]
-```
+```
The parsing itself (invoking `__call__`) is stateless. The only stateful part of a `Protocol` is the `ECU_Map`. These objects correlate OBD transmitter IDs (`tx_id`'s) with the various ECUs in the car. This way, `Message` objects can be marked with ECU constants such as:
diff --git a/obd/protocols/protocol.py b/obd/protocols/protocol.py
index efe83a9a..c8e7ab71 100644
--- a/obd/protocols/protocol.py
+++ b/obd/protocols/protocol.py
@@ -30,8 +30,11 @@
########################################################################
from binascii import hexlify
-from obd.utils import isHex, num_bits_set
-from obd.debug import debug
+from obd.utils import isHex, bitarray
+
+import logging
+
+logger = logging.getLogger(__name__)
"""
@@ -124,6 +127,7 @@ class Protocol(object):
# the TX_IDs of known ECUs
TX_ID_ENGINE = None
+ TX_ID_TRANSMISSION = None
def __init__(self, lines_0100):
@@ -146,6 +150,12 @@ def __init__(self, lines_0100):
# subsequent runs will now be tagged correctly
self.populate_ecu_map(messages)
+ # log out the ecu map
+ for tx_id, ecu in self.ecu_map.items():
+ names = [k for k in ECU.__dict__ if ECU.__dict__[k] == ecu ]
+ names = ", ".join(names)
+ logger.debug("map ECU %d --> %s" % (tx_id, names))
+
def __call__(self, lines):
"""
@@ -245,12 +255,16 @@ def populate_ecu_map(self, messages):
# if any tx_ids are exact matches to the expected values, record them
for m in messages:
+ if m.tx_id is None:
+ logger.debug("parse_frame failed to extract TX_ID")
+ continue
+
if m.tx_id == self.TX_ID_ENGINE:
self.ecu_map[m.tx_id] = ECU.ENGINE
found_engine = True
+ elif m.tx_id == self.TX_ID_TRANSMISSION:
+ self.ecu_map[m.tx_id] = ECU.TRANSMISSION
# TODO: program more of these when we figure out their constants
- # elif m.tx_id == self.TX_ID_TRANSMISSION:
- # self.ecu_map[m.tx_id] = ECU.TRANSMISSION
if not found_engine:
# last resort solution, choose ECU with the most bits set
@@ -259,7 +273,7 @@ def populate_ecu_map(self, messages):
tx_id = None
for message in messages:
- bits = sum([num_bits_set(b) for b in message.data])
+ bits = bitarray(message.data).num_set()
if bits > best:
best = bits
diff --git a/obd/protocols/protocol_can.py b/obd/protocols/protocol_can.py
index f02aa35c..e2381f79 100644
--- a/obd/protocols/protocol_can.py
+++ b/obd/protocols/protocol_can.py
@@ -31,12 +31,17 @@
from binascii import unhexlify
from obd.utils import contiguous
-from .protocol import *
+from .protocol import Protocol, Message, Frame, ECU
+
+import logging
+
+logger = logging.getLogger(__name__)
class CANProtocol(Protocol):
TX_ID_ENGINE = 0
+ TX_ID_TRANSMISSION = 1
FRAME_TYPE_SF = 0x00 # single frame
FRAME_TYPE_FF = 0x10 # first frame of multi-frame message
@@ -66,7 +71,7 @@ def parse_frame(self, frame):
# Handle odd size frames and drop
if len(raw) & 1:
- debug("Dropping frame for being odd")
+ logger.debug("Dropping frame for being odd")
return False
raw_bytes = bytearray(unhexlify(raw))
@@ -79,11 +84,11 @@ def parse_frame(self, frame):
#
# 00 00 07 E8 10 20 ...
- debug("Dropped frame for being too short")
+ logger.debug("Dropped frame for being too short")
return False
if len(raw_bytes) > 12:
- debug("Dropped frame for being too long")
+ logger.debug("Dropped frame for being too long")
return False
@@ -127,7 +132,7 @@ def parse_frame(self, frame):
if frame.type not in [self.FRAME_TYPE_SF,
self.FRAME_TYPE_FF,
self.FRAME_TYPE_CF]:
- debug("Dropping frame carrying unknown PCI frame type")
+ logger.debug("Dropping frame carrying unknown PCI frame type")
return False
@@ -169,7 +174,7 @@ def parse_message(self, message):
frame = frames[0]
if frame.type != self.FRAME_TYPE_SF:
- debug("Recieved lone frame not marked as single frame")
+ logger.debug("Recieved lone frame not marked as single frame")
return False
# extract data, ignore PCI byte and anything after the marked length
@@ -190,19 +195,19 @@ def parse_message(self, message):
elif f.type == self.FRAME_TYPE_CF:
cf.append(f)
else:
- debug("Dropping frame in multi-frame response not marked as FF or CF")
+ logger.debug("Dropping frame in multi-frame response not marked as FF or CF")
# check that we captured only one first-frame
if len(ff) > 1:
- debug("Recieved multiple frames marked FF")
+ logger.debug("Recieved multiple frames marked FF")
return False
elif len(ff) == 0:
- debug("Never received frame marked FF")
+ logger.debug("Never received frame marked FF")
return False
# check that there was at least one consecutive-frame
if len(cf) == 0:
- debug("Never received frame marked CF")
+ logger.debug("Never received frame marked CF")
return False
# calculate proper sequence indices from the lower 4 bits given
@@ -225,7 +230,7 @@ def parse_message(self, message):
# check contiguity, and that we aren't missing any frames
indices = [f.seq_index for f in cf]
if not contiguous(indices, 1, len(cf)):
- debug("Recieved multiline response with missing frames")
+ logger.debug("Recieved multiline response with missing frames")
return False
@@ -260,11 +265,15 @@ def parse_message(self, message):
message.data = message.data[:ff[0].data_len]
+ # TODO: this is an ugly solution, maybe move mode/pid byte ignoring to the decoders?
+
# chop off the Mode/PID bytes based on the mode number
mode = message.data[0]
if mode == 0x43:
- # TODO: confirm this logic. I don't have any raw test data for it yet
+ # []
+ # 43 03 11 11 22 22 33 33
+ # [DTC] [DTC] [DTC]
# fetch the DTC count, and use it as a length code
num_dtc_bytes = message.data[1] * 2
@@ -272,6 +281,12 @@ def parse_message(self, message):
# skip the PID byte and the DTC count,
message.data = message.data[2:][:num_dtc_bytes]
+ elif mode == 0x46:
+ # the monitor test mode only has a mode number
+ # the MID (mode 6's version of a PID) is repeated,
+ # and handled in the decoder
+ message.data = message.data[1:]
+
else:
# skip the Mode and PID bytes
#
diff --git a/obd/protocols/protocol_legacy.py b/obd/protocols/protocol_legacy.py
index 22abb137..1e24eeea 100644
--- a/obd/protocols/protocol_legacy.py
+++ b/obd/protocols/protocol_legacy.py
@@ -31,7 +31,11 @@
from binascii import unhexlify
from obd.utils import contiguous
-from .protocol import *
+from .protocol import Protocol, Message, Frame, ECU
+
+import logging
+
+logger = logging.getLogger(__name__)
class LegacyProtocol(Protocol):
@@ -49,17 +53,17 @@ def parse_frame(self, frame):
# Handle odd size frames and drop
if len(raw) & 1:
- debug("Dropping frame for being odd")
+ logger.debug("Dropping frame for being odd")
return False
raw_bytes = bytearray(unhexlify(raw))
if len(raw_bytes) < 6:
- debug("Dropped frame for being too short")
+ logger.debug("Dropped frame for being too short")
return False
if len(raw_bytes) > 11:
- debug("Dropped frame for being too long")
+ logger.debug("Dropped frame for being too long")
return False
# Ex.
@@ -84,15 +88,15 @@ def parse_message(self, message):
# len(frames) will always be >= 1 (see the caller, protocol.py)
mode = frames[0].data[0]
-
+
# test that all frames are responses to the same Mode (SID)
if len(frames) > 1:
if not all([mode == f.data[0] for f in frames[1:]]):
- debug("Recieved frames from multiple commands")
+ logger.debug("Recieved frames from multiple commands")
return False
# legacy protocols have different re-assembly
- # procedures for different Modes
+ # procedures for different Modes
if mode == 0x43:
# GET_DTC requests return frames with no PID or order bytes
@@ -134,7 +138,7 @@ def parse_message(self, message):
# check contiguity
indices = [f.data[2] for f in frames]
if not contiguous(indices, 1, len(frames)):
- debug("Recieved multiline response with missing frames")
+ logger.debug("Recieved multiline response with missing frames")
return False
# now that they're in order, accumulate the data from each frame
diff --git a/obd/utils.py b/obd/utils.py
index ea3b3cd3..b1061693 100644
--- a/obd/utils.py
+++ b/obd/utils.py
@@ -34,7 +34,10 @@
import string
import glob
import sys
-from .debug import debug
+import logging
+
+logger = logging.getLogger(__name__)
+
class OBDStatus:
@@ -47,15 +50,56 @@ class OBDStatus:
-def num_bits_set(n):
- return bin(n).count("1")
+class bitarray:
+ """
+ Class for representing bitarrays (inefficiently)
+
+ There's a nice C-optimized lib for this: https://github.com/ilanschnell/bitarray
+ but python-OBD doesn't use it enough to be worth adding the dependency.
+ But, if this class starts getting used too much, we should switch to that lib.
+ """
+
+ def __init__(self, _bytearray):
+ self.bits = ""
+ for b in _bytearray:
+ v = bin(b)[2:]
+ self.bits += ("0" * (8 - len(v))) + v # pad it with zeros
+
+ def __getitem__(self, key):
+ if isinstance(key, int):
+ if key >= 0 and key < len(self.bits):
+ return self.bits[key] == "1"
+ else:
+ return False
+ elif isinstance(key, slice):
+ bits = self.bits[key]
+ if bits:
+ return [ b == "1" for b in bits ]
+ else:
+ return []
+
+ def num_set(self):
+ return self.bits.count("1")
+
+ def num_cleared(self):
+ return self.bits.count("0")
+
+ def value(self, start, stop):
+ bits = self.bits[start:stop]
+ if bits:
+ return int(bits, 2)
+ else:
+ return 0
-def unhex(_hex):
- _hex = "0" if _hex == "" else _hex
- return int(_hex, 16)
+ def __len__(self):
+ return len(self.bits)
+
+ def __str__(self):
+ return self.bits
+
+ def __iter__(self):
+ return [ b == "1" for b in self.bits ].__iter__()
-def unbin(_bin):
- return int(_bin, 2)
def bytes_to_int(bs):
""" converts a big-endian byte array into a single integer """
@@ -66,13 +110,6 @@ def bytes_to_int(bs):
p += 8
return v
-def bytes_to_bits(bs):
- bits = ""
- for b in bs:
- v = bin(b)[2:]
- bits += ("0" * (8 - len(v))) + v # pad it with zeros
- return bits
-
def bytes_to_hex(bs):
h = ""
for b in bs:
@@ -80,15 +117,6 @@ def bytes_to_hex(bs):
h += ("0" * (2 - len(bh))) + bh
return h
-def bitstring(_hex, bits=None):
- b = bin(unhex(_hex))[2:]
- if bits is not None:
- b = ('0' * (bits - len(b))) + b
- return b
-
-def bitToBool(_bit):
- return (_bit == '1')
-
def twos_comp(val, num_bits):
"""compute the 2's compliment of int value val"""
if( (val&(1<<(num_bits-1))) != 0 ):
@@ -158,8 +186,3 @@ def scan_serial():
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 84d7e893..21bd121b 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@
setup(
name="obd",
- version="0.5.1",
+ version="0.6.0",
description=("Serial module for handling live sensor data from a vehicle's OBD-II port"),
classifiers=[
"Operating System :: POSIX :: Linux",
@@ -25,5 +25,5 @@
packages=find_packages(),
include_package_data=True,
zip_safe=False,
- install_requires=["pyserial"],
+ install_requires=["pyserial", "pint"],
)
diff --git a/tests/test_OBD.py b/tests/test_OBD.py
index 35b22b81..81516f2f 100644
--- a/tests/test_OBD.py
+++ b/tests/test_OBD.py
@@ -76,7 +76,7 @@ def test_is_connected():
assert not o.is_connected()
# our fake ELM class always returns success for connections
- o.port = FakeELM("/dev/null")
+ o.interface = FakeELM("/dev/null")
assert o.is_connected()
@@ -88,17 +88,17 @@ def test_status():
o = obd.OBD("/dev/null")
assert o.status() == OBDStatus.NOT_CONNECTED
- o.port = None
+ o.interface = None
assert o.status() == OBDStatus.NOT_CONNECTED
# we can manually set our fake ELM class to test
# the other values
- o.port = FakeELM("/dev/null")
+ o.interface = FakeELM("/dev/null")
- o.port._status = OBDStatus.ELM_CONNECTED
+ o.interface._status = OBDStatus.ELM_CONNECTED
assert o.status() == OBDStatus.ELM_CONNECTED
- o.port._status = OBDStatus.CAR_CONNECTED
+ o.interface._status = OBDStatus.CAR_CONNECTED
assert o.status() == OBDStatus.CAR_CONNECTED
@@ -121,31 +121,31 @@ def test_port_name():
same values as the underlying ELM327 class.
"""
o = obd.OBD("/dev/null")
- o.port = FakeELM("/dev/null")
- assert o.port_name() == o.port._portname
+ o.interface = FakeELM("/dev/null")
+ assert o.port_name() == o.interface._portname
- o.port = FakeELM("A different port name")
- assert o.port_name() == o.port._portname
+ o.interface = FakeELM("A different port name")
+ assert o.port_name() == o.interface._portname
def test_protocol_name():
o = obd.OBD("/dev/null")
- o.port = None
+ o.interface = None
assert o.protocol_name() == ""
- o.port = FakeELM("/dev/null")
- assert o.protocol_name() == o.port.protocol_name()
+ o.interface = FakeELM("/dev/null")
+ assert o.protocol_name() == o.interface.protocol_name()
def test_protocol_id():
o = obd.OBD("/dev/null")
- o.port = None
+ o.interface = None
assert o.protocol_id() == ""
- o.port = FakeELM("/dev/null")
- assert o.protocol_id() == o.port.protocol_id()
+ o.interface = FakeELM("/dev/null")
+ assert o.protocol_id() == o.interface.protocol_id()
@@ -158,30 +158,30 @@ def test_protocol_id():
def test_force():
o = obd.OBD("/dev/null", fast=False) # disable the trailing response count byte
- o.port = FakeELM("/dev/null")
+ o.interface = FakeELM("/dev/null")
r = o.query(obd.commands.RPM)
assert r.is_null()
- assert o.port._test_last_command(None)
+ assert o.interface._test_last_command(None)
r = o.query(obd.commands.RPM, force=True)
assert not r.is_null()
- assert o.port._test_last_command(obd.commands.RPM.command)
+ assert o.interface._test_last_command(obd.commands.RPM.command)
# a command that isn't in python-OBD's tables
r = o.query(command)
assert r.is_null()
- assert o.port._test_last_command(None)
+ assert o.interface._test_last_command(None)
r = o.query(command, force=True)
- assert o.port._test_last_command(command.command)
+ assert o.interface._test_last_command(command.command)
def test_fast():
o = obd.OBD("/dev/null", fast=False)
- o.port = FakeELM("/dev/null")
+ o.interface = FakeELM("/dev/null")
assert command.fast
o.query(command, force=True) # force since this command isn't in the tables
- # assert o.port._test_last_command(command.command)
+ # assert o.interface._test_last_command(command.command)
diff --git a/tests/test_OBDCommand.py b/tests/test_OBDCommand.py
index b0bb4aa7..d97c02ad 100644
--- a/tests/test_OBDCommand.py
+++ b/tests/test_OBDCommand.py
@@ -1,6 +1,6 @@
from obd.OBDCommand import OBDCommand
-from obd.OBDResponse import Unit
+from obd.UnitsAndScaling import Unit
from obd.decoders import noop
from obd.protocols import *
@@ -19,8 +19,8 @@ def test_constructor():
assert cmd.ecu == ECU.ENGINE
assert cmd.fast == False
- assert cmd.mode_int == 1
- assert cmd.pid_int == 35
+ assert cmd.mode == 1
+ assert cmd.pid == 35
# a case where "fast", and "supported" were set explicitly
# name description cmd bytes decoder ECU fast
@@ -67,18 +67,18 @@ def test_call():
-def test_get_mode_int():
+def test_get_mode():
cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE)
- assert cmd.mode_int == 0x01
+ assert cmd.mode == 0x01
cmd = OBDCommand("", "", "", "23", 4, noop, ECU.ENGINE)
- assert cmd.mode_int == 0
+ assert cmd.mode == 0
-def test_pid_int():
+def test_pid():
cmd = OBDCommand("", "", "0123", 4, noop, ECU.ENGINE)
- assert cmd.pid_int == 0x23
+ assert cmd.pid == 0x23
cmd = OBDCommand("", "", "01", 4, noop, ECU.ENGINE)
- assert cmd.pid_int == 0
+ assert cmd.pid == 0
diff --git a/tests/test_commands.py b/tests/test_commands.py
index 0ac0df2f..1b257d55 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -7,11 +7,14 @@ def test_list_integrity():
for mode, cmds in enumerate(obd.commands.modes):
for pid, cmd in enumerate(cmds):
+ if cmd is None:
+ continue # this command is reserved
+
assert cmd.command != "", "The Command's command string must not be null"
# make sure the command tables are in mode & PID order
- assert mode == cmd.mode_int, "Command is in the wrong mode list: %s" % cmd.name
- assert pid == cmd.pid_int, "The index in the list must also be the PID: %s" % cmd.name
+ assert mode == cmd.mode, "Command is in the wrong mode list: %s" % cmd.name
+ assert pid == cmd.pid, "The index in the list must also be the PID: %s" % cmd.name
# make sure all the fields are set
assert cmd.name != "", "Command names must not be null"
@@ -30,6 +33,10 @@ def test_unique_names():
for cmds in obd.commands.modes:
for cmd in cmds:
+
+ if cmd is None:
+ continue # this command is reserved
+
assert not names.__contains__(cmd.name), "Two commands share the same name: %s" % cmd.name
names[cmd.name] = True
@@ -39,9 +46,12 @@ def test_getitem():
for cmds in obd.commands.modes:
for cmd in cmds:
+ if cmd is None:
+ continue # this command is reserved
+
# by [mode][pid]
- mode = cmd.mode_int
- pid = cmd.pid_int
+ mode = cmd.mode
+ pid = cmd.pid
assert cmd == obd.commands[mode][pid], "mode %d, PID %d could not be accessed through __getitem__" % (mode, pid)
# by [name]
@@ -53,12 +63,15 @@ def test_contains():
for cmds in obd.commands.modes:
for cmd in cmds:
+ if cmd is None:
+ continue # this command is reserved
+
# by (command)
assert obd.commands.has_command(cmd)
# by (mode, pid)
- mode = cmd.mode_int
- pid = cmd.pid_int
+ mode = cmd.mode
+ pid = cmd.pid
assert obd.commands.has_pid(mode, pid)
# by (name)
@@ -67,18 +80,21 @@ def test_contains():
# by `in`
assert cmd.name in obd.commands
- # test things NOT in the tables, or invalid parameters
+ # test things NOT in the tables
assert 'modes' not in obd.commands
assert not obd.commands.has_pid(-1, 0)
assert not obd.commands.has_pid(1, -1)
- assert not obd.commands.has_command("I'm a string, not an OBDCommand")
def test_pid_getters():
# ensure that all pid getters are found
pid_getters = obd.commands.pid_getters()
- for cmds in obd.commands.modes:
- for cmd in cmds:
+ for mode in obd.commands.modes:
+ for cmd in mode:
+
+ if cmd is None:
+ continue # this command is reserved
+
if cmd.decode == pid:
assert cmd in pid_getters
diff --git a/tests/test_decoders.py b/tests/test_decoders.py
index cd6b783f..5ef0f238 100644
--- a/tests/test_decoders.py
+++ b/tests/test_decoders.py
@@ -1,8 +1,9 @@
from binascii import unhexlify
-from obd.OBDResponse import Unit
+from obd.UnitsAndScaling import Unit
from obd.protocols.protocol import Frame, Message
+from obd.codes import BASE_TESTS, COMPRESSION_TESTS, SPARK_TESTS, TEST_IDS
import obd.decoders as d
@@ -15,9 +16,12 @@ def m(hex_data, frames=[]):
return [message]
-def float_equals(d1, d2):
- values_match = (abs(d1[0] - d2[0]) < 0.02)
- units_match = (d1[1] == d2[1])
+FLOAT_EQUALS_TOLERANCE = 0.025
+
+# comparison for pint floating point values
+def float_equals(va, vb):
+ units_match = (va.u == vb.u)
+ values_match = (abs(va.magnitude - vb.magnitude) < FLOAT_EQUALS_TOLERANCE)
return values_match and units_match
@@ -25,173 +29,290 @@ def float_equals(d1, d2):
def test_noop():
- assert d.noop(m("00010203")) == (bytearray([0, 1, 2, 3]), Unit.NONE)
+ assert d.noop(m("00010203")) == bytearray([0, 1, 2, 3])
def test_drop():
- assert d.drop(m("deadbeef")) == (None, Unit.NONE)
+ assert d.drop(m("deadbeef")) == None
def test_raw_string():
- assert d.raw_string([ Message([]) ]) == ("", Unit.NONE)
- assert d.raw_string([ Message([ Frame("NO DATA") ]) ]) == ("NO DATA", Unit.NONE)
- assert d.raw_string([ Message([ Frame("A"), Frame("B") ]) ]) == ("A\nB", Unit.NONE)
- assert d.raw_string([ Message([ Frame("A") ]), Message([ Frame("B") ]) ]) == ("A\nB", Unit.NONE)
+ assert d.raw_string([ Message([]) ]) == ""
+ assert d.raw_string([ Message([ Frame("NO DATA") ]) ]) == "NO DATA"
+ assert d.raw_string([ Message([ Frame("A"), Frame("B") ]) ]) == "A\nB"
+ assert d.raw_string([ Message([ Frame("A") ]), Message([ Frame("B") ]) ]) == "A\nB"
def test_pid():
- assert d.pid(m("00000000")) == ("00000000000000000000000000000000", Unit.NONE)
- assert d.pid(m("F00AA00F")) == ("11110000000010101010000000001111", Unit.NONE)
- assert d.pid(m("11")) == ("00010001", Unit.NONE)
-
-def test_count():
- assert d.count(m("00")) == (0, Unit.COUNT)
- assert d.count(m("0F")) == (15, Unit.COUNT)
- assert d.count(m("03E8")) == (1000, Unit.COUNT)
+ assert d.pid(m("00000000")).bits == "00000000000000000000000000000000"
+ assert d.pid(m("F00AA00F")).bits == "11110000000010101010000000001111"
+ assert d.pid(m("11")).bits == "00010001"
def test_percent():
- assert d.percent(m("00")) == (0.0, Unit.PERCENT)
- assert d.percent(m("FF")) == (100.0, Unit.PERCENT)
+ assert d.percent(m("00")) == 0.0 * Unit.percent
+ assert d.percent(m("FF")) == 100.0 * Unit.percent
def test_percent_centered():
- assert d.percent_centered(m("00")) == (-100.0, Unit.PERCENT)
- assert d.percent_centered(m("80")) == (0.0, Unit.PERCENT)
- assert float_equals(d.percent_centered(m("FF")), (99.2, Unit.PERCENT))
+ assert d.percent_centered(m("00")) == -100.0 * Unit.percent
+ assert d.percent_centered(m("80")) == 0.0 * Unit.percent
+ assert float_equals(d.percent_centered(m("FF")), 99.2 * Unit.percent)
def test_temp():
- assert d.temp(m("00")) == (-40, Unit.C)
- assert d.temp(m("FF")) == (215, Unit.C)
- assert d.temp(m("03E8")) == (960, Unit.C)
-
-def test_catalyst_temp():
- assert d.catalyst_temp(m("0000")) == (-40.0, Unit.C)
- assert d.catalyst_temp(m("FFFF")) == (6513.5, Unit.C)
+ assert d.temp(m("00")) == Unit.Quantity(-40, Unit.celsius)
+ assert d.temp(m("FF")) == Unit.Quantity(215, Unit.celsius)
+ assert d.temp(m("03E8")) == Unit.Quantity(960, Unit.celsius)
def test_current_centered():
- assert d.current_centered(m("00000000")) == (-128.0, Unit.MA)
- assert d.current_centered(m("00008000")) == (0.0, Unit.MA)
- assert float_equals(d.current_centered(m("0000FFFF")), (128.0, Unit.MA))
- assert d.current_centered(m("ABCD8000")) == (0.0, Unit.MA) # first 2 bytes are unused (should be disregarded)
+ assert d.current_centered(m("00000000")) == -128.0 * Unit.milliampere
+ assert d.current_centered(m("00008000")) == 0.0 * Unit.milliampere
+ assert d.current_centered(m("ABCD8000")) == 0.0 * Unit.milliampere # first 2 bytes are unused (should be disregarded)
+ assert float_equals(d.current_centered(m("0000FFFF")), 128.0 * Unit.milliampere)
def test_sensor_voltage():
- assert d.sensor_voltage(m("0000")) == (0.0, Unit.VOLT)
- assert d.sensor_voltage(m("FFFF")) == (1.275, Unit.VOLT)
+ assert d.sensor_voltage(m("0000")) == 0.0 * Unit.volt
+ assert d.sensor_voltage(m("FFFF")) == 1.275 * Unit.volt
def test_sensor_voltage_big():
- assert d.sensor_voltage_big(m("00000000")) == (0.0, Unit.VOLT)
- assert float_equals(d.sensor_voltage_big(m("00008000")), (4.0, Unit.VOLT))
- assert d.sensor_voltage_big(m("0000FFFF")) == (8.0, Unit.VOLT)
- assert d.sensor_voltage_big(m("ABCD0000")) == (0.0, Unit.VOLT) # first 2 bytes are unused (should be disregarded)
+ assert d.sensor_voltage_big(m("00000000")) == 0.0 * Unit.volt
+ assert float_equals(d.sensor_voltage_big(m("00008000")), 4.0 * Unit.volt)
+ assert d.sensor_voltage_big(m("0000FFFF")) == 8.0 * Unit.volt
+ assert d.sensor_voltage_big(m("ABCD0000")) == 0.0 * Unit.volt # first 2 bytes are unused (should be disregarded)
def test_fuel_pressure():
- assert d.fuel_pressure(m("00")) == (0, Unit.KPA)
- assert d.fuel_pressure(m("80")) == (384, Unit.KPA)
- assert d.fuel_pressure(m("FF")) == (765, Unit.KPA)
+ assert d.fuel_pressure(m("00")) == 0 * Unit.kilopascal
+ assert d.fuel_pressure(m("80")) == 384 * Unit.kilopascal
+ assert d.fuel_pressure(m("FF")) == 765 * Unit.kilopascal
def test_pressure():
- assert d.pressure(m("00")) == (0, Unit.KPA)
- assert d.pressure(m("00")) == (0, Unit.KPA)
-
-def test_fuel_pres_vac():
- assert d.fuel_pres_vac(m("0000")) == (0.0, Unit.KPA)
- assert d.fuel_pres_vac(m("FFFF")) == (5177.265, Unit.KPA)
-
-def test_fuel_pres_direct():
- assert d.fuel_pres_direct(m("0000")) == (0, Unit.KPA)
- assert d.fuel_pres_direct(m("FFFF")) == (655350, Unit.KPA)
+ assert d.pressure(m("00")) == 0 * Unit.kilopascal
+ assert d.pressure(m("00")) == 0 * Unit.kilopascal
def test_evap_pressure():
pass # TODO
- #assert d.evap_pressure(m("0000")) == (0.0, Unit.PA)
+ #assert d.evap_pressure(m("0000")) == 0.0 * Unit.PA)
def test_abs_evap_pressure():
- assert d.abs_evap_pressure(m("0000")) == (0, Unit.KPA)
- assert d.abs_evap_pressure(m("FFFF")) == (327.675, Unit.KPA)
+ assert d.abs_evap_pressure(m("0000")) == 0 * Unit.kilopascal
+ assert d.abs_evap_pressure(m("FFFF")) == 327.675 * Unit.kilopascal
def test_evap_pressure_alt():
- assert d.evap_pressure_alt(m("0000")) == (-32767, Unit.PA)
- assert d.evap_pressure_alt(m("7FFF")) == (0, Unit.PA)
- assert d.evap_pressure_alt(m("FFFF")) == (32768, Unit.PA)
-
-def test_rpm():
- assert d.rpm(m("0000")) == (0.0, Unit.RPM)
- assert d.rpm(m("FFFF")) == (16383.75, Unit.RPM)
-
-def test_speed():
- assert d.speed(m("00")) == (0, Unit.KPH)
- assert d.speed(m("FF")) == (255, Unit.KPH)
+ assert d.evap_pressure_alt(m("0000")) == -32767 * Unit.pascal
+ assert d.evap_pressure_alt(m("7FFF")) == 0 * Unit.pascal
+ assert d.evap_pressure_alt(m("FFFF")) == 32768 * Unit.pascal
def test_timing_advance():
- assert d.timing_advance(m("00")) == (-64.0, Unit.DEGREES)
- assert d.timing_advance(m("FF")) == (63.5, Unit.DEGREES)
+ assert d.timing_advance(m("00")) == -64.0 * Unit.degrees
+ assert d.timing_advance(m("FF")) == 63.5 * Unit.degrees
def test_inject_timing():
- assert d.inject_timing(m("0000")) == (-210, Unit.DEGREES)
- assert float_equals(d.inject_timing(m("FFFF")), (302, Unit.DEGREES))
-
-def test_maf():
- assert d.maf(m("0000")) == (0.0, Unit.GPS)
- assert d.maf(m("FFFF")) == (655.35, Unit.GPS)
+ assert d.inject_timing(m("0000")) == -210 * Unit.degrees
+ assert float_equals(d.inject_timing(m("FFFF")), 302 * Unit.degrees)
def test_max_maf():
- assert d.max_maf(m("00000000")) == (0, Unit.GPS)
- assert d.max_maf(m("FF000000")) == (2550, Unit.GPS)
- assert d.max_maf(m("00ABCDEF")) == (0, Unit.GPS) # last 3 bytes are unused (should be disregarded)
-
-def test_seconds():
- assert d.seconds(m("0000")) == (0, Unit.SEC)
- assert d.seconds(m("FFFF")) == (65535, Unit.SEC)
-
-def test_minutes():
- assert d.minutes(m("0000")) == (0, Unit.MIN)
- assert d.minutes(m("FFFF")) == (65535, Unit.MIN)
-
-def test_distance():
- assert d.distance(m("0000")) == (0, Unit.KM)
- assert d.distance(m("FFFF")) == (65535, Unit.KM)
+ assert d.max_maf(m("00000000")) == 0 * Unit.grams_per_second
+ assert d.max_maf(m("FF000000")) == 2550 * Unit.grams_per_second
+ assert d.max_maf(m("00ABCDEF")) == 0 * Unit.grams_per_second # last 3 bytes are unused (should be disregarded)
def test_fuel_rate():
- assert d.fuel_rate(m("0000")) == (0.0, Unit.LPH)
- assert d.fuel_rate(m("FFFF")) == (3276.75, Unit.LPH)
+ assert d.fuel_rate(m("0000")) == 0.0 * Unit.liters_per_hour
+ assert d.fuel_rate(m("FFFF")) == 3276.75 * Unit.liters_per_hour
def test_fuel_status():
- assert d.fuel_status(m("0100")) == ("Open loop due to insufficient engine temperature", Unit.NONE)
- assert d.fuel_status(m("0800")) == ("Open loop due to system failure", Unit.NONE)
- assert d.fuel_status(m("0300")) == (None, Unit.NONE)
+ assert d.fuel_status(m("0100")) == ("Open loop due to insufficient engine temperature", "")
+ assert d.fuel_status(m("0800")) == ("Open loop due to system failure", "")
+ assert d.fuel_status(m("0808")) == ("Open loop due to system failure",
+ "Open loop due to system failure")
+ assert d.fuel_status(m("0008")) == ("", "Open loop due to system failure")
+ assert d.fuel_status(m("0000")) == None
+ assert d.fuel_status(m("0300")) == None
+ assert d.fuel_status(m("0303")) == None
def test_air_status():
- assert d.air_status(m("01")) == ("Upstream", Unit.NONE)
- assert d.air_status(m("08")) == ("Pump commanded on for diagnostics", Unit.NONE)
- assert d.air_status(m("03")) == (None, Unit.NONE)
+ assert d.air_status(m("01")) == "Upstream"
+ assert d.air_status(m("08")) == "Pump commanded on for diagnostics"
+ assert d.air_status(m("03")) == None
+
+def test_o2_sensors():
+ assert d.o2_sensors(m("00")) == ((),(False, False, False, False), (False, False, False, False))
+ assert d.o2_sensors(m("01")) == ((),(False, False, False, False), (False, False, False, True))
+ assert d.o2_sensors(m("0F")) == ((),(False, False, False, False), (True, True, True, True))
+ assert d.o2_sensors(m("F0")) == ((),(True, True, True, True), (False, False, False, False))
+
+def test_o2_sensors_alt():
+ assert d.o2_sensors_alt(m("00")) == ((),(False, False), (False, False), (False, False), (False, False))
+ assert d.o2_sensors_alt(m("01")) == ((),(False, False), (False, False), (False, False), (False, True))
+ assert d.o2_sensors_alt(m("0F")) == ((),(False, False), (False, False), (True, True), (True, True))
+ assert d.o2_sensors_alt(m("F0")) == ((),(True, True), (True, True), (False, False), (False, False))
+
+def test_aux_input_status():
+ assert d.aux_input_status(m("00")) == False
+ assert d.aux_input_status(m("80")) == True
+
+def test_absolute_load():
+ assert d.absolute_load(m("0000")) == 0 * Unit.percent
+ assert d.absolute_load(m("FFFF")) == 25700 * Unit.percent
def test_elm_voltage():
# these aren't parsed as standard hex messages, so manufacture our own
- assert d.elm_voltage([ Message([ Frame("12.875") ]) ]) == (12.875, Unit.VOLT)
- assert d.elm_voltage([ Message([ Frame("12") ]) ]) == (12, Unit.VOLT)
- assert d.elm_voltage([ Message([ Frame("12ABCD") ]) ]) == (None, Unit.NONE)
+ assert d.elm_voltage([ Message([ Frame("12.875") ]) ]) == 12.875 * Unit.volt
+ assert d.elm_voltage([ Message([ Frame("12") ]) ]) == 12 * Unit.volt
+ assert d.elm_voltage([ Message([ Frame("12ABCD") ]) ]) == None
+
+def test_status():
+ status = d.status(m("8307FF00"))
+ assert status.MIL
+ assert status.DTC_count == 3
+ assert status.ignition_type == "spark"
+
+ for name in BASE_TESTS:
+ assert status.__dict__[name].available
+ assert status.__dict__[name].complete
+
+ # check that NONE of the compression tests are available
+ for name in COMPRESSION_TESTS:
+ if name and name not in SPARK_TESTS: # there's one test name in common between spark/compression
+ assert not status.__dict__[name].available
+ assert not status.__dict__[name].complete
+
+ # check that ALL of the spark tests are availablex
+ for name in SPARK_TESTS:
+ if name:
+ assert status.__dict__[name].available
+ assert status.__dict__[name].complete
+
+ # a different test
+ status = d.status(m("00790303"))
+ assert not status.MIL
+ assert status.DTC_count == 0
+ assert status.ignition_type == "compression"
+
+ # availability
+ assert status.MISFIRE_MONITORING.available
+ assert not status.FUEL_SYSTEM_MONITORING.available
+ assert not status.COMPONENT_MONITORING.available
+
+ # completion
+ assert not status.MISFIRE_MONITORING.complete
+ assert not status.FUEL_SYSTEM_MONITORING.complete
+ assert not status.COMPONENT_MONITORING.complete
+
+ # check that NONE of the spark tests are availablex
+ for name in SPARK_TESTS:
+ if name and name not in COMPRESSION_TESTS:
+ assert not status.__dict__[name].available
+ assert not status.__dict__[name].complete
+
+ # availability
+ assert status.NMHC_CATALYST_MONITORING.available
+ assert status.NOX_SCR_AFTERTREATMENT_MONITORING.available
+ assert not status.BOOST_PRESSURE_MONITORING.available
+ assert not status.EXHAUST_GAS_SENSOR_MONITORING.available
+ assert not status.PM_FILTER_MONITORING.available
+ assert not status.EGR_VVT_SYSTEM_MONITORING.available
+
+ # completion
+ assert not status.NMHC_CATALYST_MONITORING.complete
+ assert not status.NOX_SCR_AFTERTREATMENT_MONITORING.complete
+ assert status.BOOST_PRESSURE_MONITORING.complete
+ assert status.EXHAUST_GAS_SENSOR_MONITORING.complete
+ assert status.PM_FILTER_MONITORING.complete
+ assert status.EGR_VVT_SYSTEM_MONITORING.complete
+
+
+def test_single_dtc():
+ assert d.single_dtc(m("0104")) == ("P0104", "Mass or Volume Air Flow Circuit Intermittent")
+ assert d.single_dtc(m("4123")) == ("C0123", "") # reverse back into correct bit-order
+ assert d.single_dtc(m("01")) == None
+ assert d.single_dtc(m("010400")) == None
def test_dtc():
- assert d.dtc(m("0104")) == ([
+ assert d.dtc(m("0104")) == [
("P0104", "Mass or Volume Air Flow Circuit Intermittent"),
- ], Unit.NONE)
+ ]
# multiple codes
- assert d.dtc(m("010480034123")) == ([
+ assert d.dtc(m("010480034123")) == [
("P0104", "Mass or Volume Air Flow Circuit Intermittent"),
- ("B0003", "Unknown error code"),
- ("C0123", "Unknown error code"),
- ], Unit.NONE)
+ ("B0003", ""), # unknown error codes return empty strings
+ ("C0123", ""),
+ ]
# invalid code lengths are dropped
- assert d.dtc(m("0104800341")) == ([
+ assert d.dtc(m("0104800341")) == [
("P0104", "Mass or Volume Air Flow Circuit Intermittent"),
- ("B0003", "Unknown error code"),
- ], Unit.NONE)
+ ("B0003", ""),
+ ]
# 0000 codes are dropped
- assert d.dtc(m("000001040000")) == ([
+ assert d.dtc(m("000001040000")) == [
("P0104", "Mass or Volume Air Flow Circuit Intermittent"),
- ], Unit.NONE)
+ ]
# test multiple messages
- assert d.dtc(m("0104") + m("8003") + m("0000")) == ([
+ assert d.dtc(m("0104") + m("8003") + m("0000")) == [
("P0104", "Mass or Volume Air Flow Circuit Intermittent"),
- ("B0003", "Unknown error code"),
- ], Unit.NONE)
+ ("B0003", ""),
+ ]
+
+def test_monitor():
+ # single test -----------------------------------------
+ # [ test ]
+ v = d.monitor(m("01010A0BB00BB00BB0"))
+ assert len(v) == 1 # 1 test result
+
+ # make sure we can look things up by name and TID
+ assert v[0x01] == v.RTL_THRESHOLD_VOLTAGE == v["RTL_THRESHOLD_VOLTAGE"]
+
+ # make sure we got information
+ assert not v[0x01].is_null()
+
+ assert float_equals(v[0x01].value, 365 * Unit.millivolt)
+ assert float_equals(v[0x01].min, 365 * Unit.millivolt)
+ assert float_equals(v[0x01].max, 365 * Unit.millivolt)
+
+ # multiple tests --------------------------------------
+ # [ test ][ test ][ test ]
+ v = d.monitor(m("01010A0BB00BB00BB00105100048000000640185240096004BFFFF"))
+ assert len(v) == 3 # 3 test results
+
+ # make sure we can look things up by name and TID
+ assert v[0x01] == v.RTL_THRESHOLD_VOLTAGE == v["RTL_THRESHOLD_VOLTAGE"]
+ assert v[0x05] == v.RTL_SWITCH_TIME == v["RTL_SWITCH_TIME"]
+
+ # make sure we got information
+ assert not v[0x01].is_null()
+ assert not v[0x05].is_null()
+ assert not v[0x85].is_null()
+
+ assert float_equals(v[0x01].value, 365 * Unit.millivolt)
+ assert float_equals(v[0x01].min, 365 * Unit.millivolt)
+ assert float_equals(v[0x01].max, 365 * Unit.millivolt)
+
+ assert float_equals(v[0x05].value, 72 * Unit.millisecond)
+ assert float_equals(v[0x05].min, 0 * Unit.millisecond)
+ assert float_equals(v[0x05].max, 100 * Unit.millisecond)
+
+ assert float_equals(v[0x85].value, 150 * Unit.count)
+ assert float_equals(v[0x85].min, 75 * Unit.count)
+ assert float_equals(v[0x85].max, 65535 * Unit.count)
+
+ # truncate incomplete tests ----------------------------
+ # [ test ][junk]
+ v = d.monitor(m("01010A0BB00BB00BB0ABCDEF"))
+ assert len(v) == 1 # 1 test result
+
+ # make sure we can look things up by name and TID
+ assert v[0x01] == v.RTL_THRESHOLD_VOLTAGE == v["RTL_THRESHOLD_VOLTAGE"]
+
+ # make sure we got information
+ assert not v[0x01].is_null()
+
+ assert float_equals(v[0x01].value, 365 * Unit.millivolt)
+ assert float_equals(v[0x01].min, 365 * Unit.millivolt)
+ assert float_equals(v[0x01].max, 365 * Unit.millivolt)
+
+ # truncate incomplete tests ----------------------------
+ v = d.monitor(m("01010A0BB00BB00B"))
+ assert len(v) == 0 # no valid tests
+
+ # make sure that the standard tests are null
+ for tid in TEST_IDS:
+ name = TEST_IDS[tid][0]
+ assert v[tid].is_null()
diff --git a/tests/test_elm327.py b/tests/test_elm327.py
deleted file mode 100644
index 781befc6..00000000
--- a/tests/test_elm327.py
+++ /dev/null
@@ -1,4 +0,0 @@
-
-from obd.protocols import ECU, SAE_J1850_PWM
-from obd.elm327 import ELM327
-
diff --git a/tests/test_obdsim.py b/tests/test_obdsim.py
index 287ade6c..d94862dc 100644
--- a/tests/test_obdsim.py
+++ b/tests/test_obdsim.py
@@ -3,7 +3,7 @@
import pytest
from obd import commands, Unit
-STANDARD_WAIT_TIME = 0.25
+STANDARD_WAIT_TIME = 0.2
@pytest.fixture(scope="module")
@@ -11,12 +11,6 @@ def obd(request):
"""provides an OBD connection object for obdsim"""
import obd
port = request.config.getoption("--port")
-
- # TODO: lookup how to fail inside of a fixture
- if port is None:
- print("Please run obdsim and use --port=")
- exit(1)
-
return obd.OBD(port)
@@ -25,25 +19,24 @@ 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
+ return (not r.is_null()) and \
+ (r.value.u == Unit.rpm) and \
+ (r.value >= 0.0 * Unit.rpm)
+
+@pytest.mark.skipif(not pytest.config.getoption("--port"),
+ reason="needs --port= to run")
def test_supports(obd):
assert(len(obd.supported_commands) > 0)
assert(obd.supports(commands.RPM))
+@pytest.mark.skipif(not pytest.config.getoption("--port"),
+ reason="needs --port= to run")
def test_rpm(obd):
r = obd.query(commands.RPM)
assert(good_rpm_response(r))
@@ -51,6 +44,8 @@ def test_rpm(obd):
# Async tests
+@pytest.mark.skipif(not pytest.config.getoption("--port"),
+ reason="needs --port= to run")
def test_async_query(async):
rs = []
@@ -69,6 +64,8 @@ def test_async_query(async):
assert(all([ good_rpm_response(r) for r in rs ]))
+@pytest.mark.skipif(not pytest.config.getoption("--port"),
+ reason="needs --port= to run")
def test_async_callback(async):
rs = []
@@ -83,6 +80,8 @@ def test_async_callback(async):
assert(all([ good_rpm_response(r) for r in rs ]))
+@pytest.mark.skipif(not pytest.config.getoption("--port"),
+ reason="needs --port= to run")
def test_async_paused(async):
assert(not async.running)
@@ -99,6 +98,8 @@ def test_async_paused(async):
assert(not async.running)
+@pytest.mark.skipif(not pytest.config.getoption("--port"),
+ reason="needs --port= to run")
def test_async_unwatch(async):
watched_rs = []
@@ -129,6 +130,8 @@ def test_async_unwatch(async):
assert(all([ r.is_null() for r in unwatched_rs ]))
+@pytest.mark.skipif(not pytest.config.getoption("--port"),
+ reason="needs --port= to run")
def test_async_unwatch_callback(async):
a_rs = []
diff --git a/tests/test_protocol.py b/tests/test_protocol.py
index a7f95a1a..b623d58b 100644
--- a/tests/test_protocol.py
+++ b/tests/test_protocol.py
@@ -1,6 +1,5 @@
import random
-from obd.utils import unhex
from obd.protocols import *
from obd.protocols.protocol import Frame, Message
@@ -55,10 +54,10 @@ def test_message_hex():
message.data = b'\x00\x01\x02'
assert message.hex() == b'000102'
- assert unhex(message.hex()[0:2]) == 0x00
- assert unhex(message.hex()[2:4]) == 0x01
- assert unhex(message.hex()[4:6]) == 0x02
- assert unhex(message.hex()) == 0x000102
+ assert int(message.hex()[0:2], 16) == 0x00
+ assert int(message.hex()[2:4], 16) == 0x01
+ assert int(message.hex()[4:6], 16) == 0x02
+ assert int(message.hex(), 16) == 0x000102
def test_populate_ecu_map():
diff --git a/tests/test_protocol_can.py b/tests/test_protocol_can.py
index cfa10789..4392898f 100644
--- a/tests/test_protocol_can.py
+++ b/tests/test_protocol_can.py
@@ -201,6 +201,27 @@ def test_multi_line_mode_03():
check_message(r[0], len(test_case), 0, correct_data)
+def test_multi_line_mode_06():
+ """
+ Tests the special handling of mode 6 commands.
+ The parser should chop off only the Mode byte from the response.
+ """
+
+ for protocol in CAN_11_PROTOCOLS:
+ p = protocol([])
+
+ test_case = [
+ "7E8 10 0A 46 01 01 0A 0B B0",
+ "7E8 21 0B B0 0B B0",
+ ]
+
+ correct_data = [0x01, 0x01, 0x0A, 0x0B, 0xB0, 0x0B, 0xB0, 0x0B, 0xB0]
+
+ r = p(test_case)
+ assert len(r) == 1
+ check_message(r[0], len(test_case), 0, correct_data)
+
+
def test_can_29():
pass
diff --git a/tests/test_uas.py b/tests/test_uas.py
new file mode 100644
index 00000000..142f9426
--- /dev/null
+++ b/tests/test_uas.py
@@ -0,0 +1,573 @@
+
+from binascii import unhexlify
+from obd.UnitsAndScaling import Unit, UAS_IDS
+
+
+# shim to convert human-readable hex into bytearray
+def b(_hex):
+ return bytearray(unhexlify(_hex))
+
+FLOAT_EQUALS_TOLERANCE = 0.025
+
+# comparison for pint floating point values
+def float_equals(va, vb):
+ units_match = (va.u == vb.u)
+ values_match = (abs(va.magnitude - vb.magnitude) < FLOAT_EQUALS_TOLERANCE)
+ return values_match and units_match
+
+
+"""
+Unsigned Units
+"""
+
+def test_01():
+ assert UAS_IDS[0x01](b("0000")) == 0 * Unit.count
+ assert UAS_IDS[0x01](b("0001")) == 1 * Unit.count
+ assert UAS_IDS[0x01](b("FFFF")) == 65535 * Unit.count
+
+def test_02():
+ assert UAS_IDS[0x02](b("0000")) == 0 * Unit.count
+ assert UAS_IDS[0x02](b("0001")) == 0.1 * Unit.count
+ assert UAS_IDS[0x02](b("FFFF")) == 6553.5 * Unit.count
+
+def test_03():
+ assert UAS_IDS[0x03](b("0000")) == 0 * Unit.count
+ assert UAS_IDS[0x03](b("0001")) == 0.01 * Unit.count
+ assert UAS_IDS[0x03](b("FFFF")) == 655.35 * Unit.count
+
+def test_04():
+ assert UAS_IDS[0x04](b("0000")) == 0 * Unit.count
+ assert UAS_IDS[0x04](b("0001")) == 0.001 * Unit.count
+ assert UAS_IDS[0x04](b("FFFF")) == 65.535 * Unit.count
+
+def test_05():
+ assert float_equals(UAS_IDS[0x05](b("0000")), 0 * Unit.count)
+ assert float_equals(UAS_IDS[0x05](b("0001")), 0.0000305 * Unit.count)
+ assert float_equals(UAS_IDS[0x05](b("FFFF")), 1.9999 * Unit.count)
+
+def test_06():
+ assert float_equals(UAS_IDS[0x06](b("0000")), 0 * Unit.count)
+ assert float_equals(UAS_IDS[0x06](b("0001")), 0.000305 * Unit.count)
+ assert float_equals(UAS_IDS[0x06](b("FFFF")), 19.988 * Unit.count)
+
+def test_07():
+ assert float_equals(UAS_IDS[0x07](b("0000")), 0 * Unit.rpm)
+ assert float_equals(UAS_IDS[0x07](b("0002")), 0.5 * Unit.rpm)
+ assert float_equals(UAS_IDS[0x07](b("FFFD")), 16383.25 * Unit.rpm)
+ assert float_equals(UAS_IDS[0x07](b("FFFF")), 16383.75 * Unit.rpm)
+
+def test_08():
+ assert float_equals(UAS_IDS[0x08](b("0000")), 0 * Unit.kph)
+ assert float_equals(UAS_IDS[0x08](b("0064")), 1 * Unit.kph)
+ assert float_equals(UAS_IDS[0x08](b("03E7")), 9.99 * Unit.kph)
+ assert float_equals(UAS_IDS[0x08](b("FFFF")), 655.35 * Unit.kph)
+
+def test_09():
+ assert float_equals(UAS_IDS[0x09](b("0000")), 0 * Unit.kph)
+ assert float_equals(UAS_IDS[0x09](b("0064")), 100 * Unit.kph)
+ assert float_equals(UAS_IDS[0x09](b("03E7")), 999 * Unit.kph)
+ assert float_equals(UAS_IDS[0x09](b("FFFF")), 65535 * Unit.kph)
+
+def test_0A():
+ # the standard gives example values that don't line up perfectly
+ # with the scale. The last two tests here deviate from the standard
+ assert float_equals(UAS_IDS[0x0A](b("0000")), 0 * Unit.millivolt)
+ assert float_equals(UAS_IDS[0x0A](b("0001")), 0.122 * Unit.millivolt)
+ assert float_equals(UAS_IDS[0x0A](b("2004")), 999.912 * Unit.millivolt) # 1000.488 mV
+ assert float_equals(UAS_IDS[0x0A](b("FFFF")), 7995.27 * Unit.millivolt) # 7999 mV
+
+def test_0B():
+ assert UAS_IDS[0x0B](b("0000")) == 0 * Unit.volt
+ assert UAS_IDS[0x0B](b("0001")) == 0.001 * Unit.volt
+ assert UAS_IDS[0x0B](b("FFFF")) == 65.535 * Unit.volt
+
+def test_0C():
+ assert float_equals(UAS_IDS[0x0C](b("0000")), 0 * Unit.volt)
+ assert float_equals(UAS_IDS[0x0C](b("0001")), 0.01 * Unit.volt)
+ assert float_equals(UAS_IDS[0x0C](b("FFFF")), 655.350 * Unit.volt)
+
+def test_0D():
+ assert float_equals(UAS_IDS[0x0D](b("0000")), 0 * Unit.milliampere)
+ assert float_equals(UAS_IDS[0x0D](b("0001")), 0.004 * Unit.milliampere)
+ assert float_equals(UAS_IDS[0x0D](b("8000")), 128 * Unit.milliampere)
+ assert float_equals(UAS_IDS[0x0D](b("FFFF")), 255.996 * Unit.milliampere)
+
+def test_0E():
+ assert UAS_IDS[0x0E](b("0000")) == 0 * Unit.ampere
+ assert UAS_IDS[0x0E](b("8000")) == 32.768 * Unit.ampere
+ assert UAS_IDS[0x0E](b("FFFF")) == 65.535 * Unit.ampere
+
+def test_0F():
+ assert UAS_IDS[0x0F](b("0000")) == 0 * Unit.ampere
+ assert UAS_IDS[0x0F](b("0001")) == 0.01 * Unit.ampere
+ assert UAS_IDS[0x0F](b("FFFF")) == 655.35 * Unit.ampere
+
+def test_10():
+ assert UAS_IDS[0x10](b("0000")) == 0 * Unit.millisecond
+ assert UAS_IDS[0x10](b("8000")) == 32768 * Unit.millisecond
+ assert UAS_IDS[0x10](b("EA60")) == 60000 * Unit.millisecond
+ assert UAS_IDS[0x10](b("FFFF")) == 65535 * Unit.millisecond
+
+def test_11():
+ assert UAS_IDS[0x11](b("0000")) == 0 * Unit.millisecond
+ assert UAS_IDS[0x11](b("8000")) == 3276800 * Unit.millisecond
+ assert UAS_IDS[0x11](b("EA60")) == 6000000 * Unit.millisecond
+ assert UAS_IDS[0x11](b("FFFF")) == 6553500 * Unit.millisecond
+
+def test_12():
+ assert UAS_IDS[0x12](b("0000")) == 0 * Unit.second
+ assert UAS_IDS[0x12](b("003C")) == 60 * Unit.second
+ assert UAS_IDS[0x12](b("0E10")) == 3600 * Unit.second
+ assert UAS_IDS[0x12](b("FFFF")) == 65535 * Unit.second
+
+def test_13():
+ assert UAS_IDS[0x13](b("0000")) == 0 * Unit.milliohm
+ assert UAS_IDS[0x13](b("0001")) == 1 * Unit.milliohm
+ assert UAS_IDS[0x13](b("8000")) == 32768 * Unit.milliohm
+ assert UAS_IDS[0x13](b("FFFF")) == 65535 * Unit.milliohm
+
+def test_14():
+ assert UAS_IDS[0x14](b("0000")) == 0 * Unit.ohm
+ assert UAS_IDS[0x14](b("0001")) == 1 * Unit.ohm
+ assert UAS_IDS[0x14](b("8000")) == 32768 * Unit.ohm
+ assert UAS_IDS[0x14](b("FFFF")) == 65535 * Unit.ohm
+
+def test_15():
+ assert UAS_IDS[0x15](b("0000")) == 0 * Unit.kiloohm
+ assert UAS_IDS[0x15](b("0001")) == 1 * Unit.kiloohm
+ assert UAS_IDS[0x15](b("8000")) == 32768 * Unit.kiloohm
+ assert UAS_IDS[0x15](b("FFFF")) == 65535 * Unit.kiloohm
+
+def test_16():
+ assert UAS_IDS[0x16](b("0000")) == Unit.Quantity(-40, Unit.celsius)
+ assert UAS_IDS[0x16](b("0001")) == Unit.Quantity(-39.9, Unit.celsius)
+ assert UAS_IDS[0x16](b("00DC")) == Unit.Quantity(-18, Unit.celsius)
+ assert UAS_IDS[0x16](b("0190")) == Unit.Quantity(0, Unit.celsius)
+ assert UAS_IDS[0x16](b("FFFF")) == Unit.Quantity(6513.5, Unit.celsius)
+
+def test_17():
+ assert UAS_IDS[0x17](b("0000")) == 0 * Unit.kilopascal
+ assert UAS_IDS[0x17](b("0001")) == 0.01 * Unit.kilopascal
+ assert UAS_IDS[0x17](b("FFFF")) == 655.35 * Unit.kilopascal
+
+def test_18():
+ assert UAS_IDS[0x18](b("0000")) == 0 * Unit.kilopascal
+ assert UAS_IDS[0x18](b("0001")) == 0.0117 * Unit.kilopascal
+ assert UAS_IDS[0x18](b("FFFF")) == 766.7595 * Unit.kilopascal
+
+def test_19():
+ assert UAS_IDS[0x19](b("0000")) == 0 * Unit.kilopascal
+ assert UAS_IDS[0x19](b("0001")) == 0.079 * Unit.kilopascal
+ assert UAS_IDS[0x19](b("FFFF")) == 5177.265 * Unit.kilopascal
+
+def test_1A():
+ assert UAS_IDS[0x1A](b("0000")) == 0 * Unit.kilopascal
+ assert UAS_IDS[0x1A](b("0001")) == 1 * Unit.kilopascal
+ assert UAS_IDS[0x1A](b("FFFF")) == 65535 * Unit.kilopascal
+
+def test_1B():
+ assert UAS_IDS[0x1B](b("0000")) == 0 * Unit.kilopascal
+ assert UAS_IDS[0x1B](b("0001")) == 10 * Unit.kilopascal
+ assert UAS_IDS[0x1B](b("FFFF")) == 655350 * Unit.kilopascal
+
+def test_1C():
+ assert UAS_IDS[0x1C](b("0000")) == 0 * Unit.degree
+ assert UAS_IDS[0x1C](b("0001")) == 0.01 * Unit.degree
+ assert UAS_IDS[0x1C](b("8CA0")) == 360 * Unit.degree
+ assert UAS_IDS[0x1C](b("FFFF")) == 655.35 * Unit.degree
+
+def test_1D():
+ assert UAS_IDS[0x1D](b("0000")) == 0 * Unit.degree
+ assert UAS_IDS[0x1D](b("0001")) == 0.5 * Unit.degree
+ assert UAS_IDS[0x1D](b("FFFF")) == 32767.5 * Unit.degree
+
+def test_1E():
+ assert float_equals(UAS_IDS[0x1E](b("0000")), 0 * Unit.ratio)
+ assert float_equals(UAS_IDS[0x1E](b("8013")), 1 * Unit.ratio)
+ assert float_equals(UAS_IDS[0x1E](b("FFFF")), 1.999 * Unit.ratio)
+
+def test_1F():
+ assert float_equals(UAS_IDS[0x1F](b("0000")), 0 * Unit.ratio)
+ assert float_equals(UAS_IDS[0x1F](b("0001")), 0.05 * Unit.ratio)
+ assert float_equals(UAS_IDS[0x1F](b("0014")), 1 * Unit.ratio)
+ assert float_equals(UAS_IDS[0x1F](b("0126")), 14.7 * Unit.ratio)
+ assert float_equals(UAS_IDS[0x1F](b("FFFF")), 3276.75 * Unit.ratio)
+
+def test_20():
+ assert float_equals(UAS_IDS[0x20](b("0000")), 0 * Unit.ratio)
+ assert float_equals(UAS_IDS[0x20](b("0001")), 0.0039062 * Unit.ratio)
+ assert float_equals(UAS_IDS[0x20](b("FFFF")), 255.993 * Unit.ratio)
+
+def test_21():
+ assert UAS_IDS[0x21](b("0000")) == 0 * Unit.millihertz
+ assert UAS_IDS[0x21](b("8000")) == 32768 * Unit.millihertz
+ assert UAS_IDS[0x21](b("FFFF")) == 65535 * Unit.millihertz
+
+def test_22():
+ assert UAS_IDS[0x22](b("0000")) == 0 * Unit.hertz
+ assert UAS_IDS[0x22](b("8000")) == 32768 * Unit.hertz
+ assert UAS_IDS[0x22](b("FFFF")) == 65535 * Unit.hertz
+
+def test_23():
+ assert UAS_IDS[0x23](b("0000")) == 0 * Unit.kilohertz
+ assert UAS_IDS[0x23](b("8000")) == 32768 * Unit.kilohertz
+ assert UAS_IDS[0x23](b("FFFF")) == 65535 * Unit.kilohertz
+
+def test_24():
+ assert UAS_IDS[0x24](b("0000")) == 0 * Unit.count
+ assert UAS_IDS[0x24](b("0001")) == 1 * Unit.count
+ assert UAS_IDS[0x24](b("FFFF")) == 65535 * Unit.count
+
+def test_25():
+ assert UAS_IDS[0x25](b("0000")) == 0 * Unit.kilometer
+ assert UAS_IDS[0x25](b("0001")) == 1 * Unit.kilometer
+ assert UAS_IDS[0x25](b("FFFF")) == 65535 * Unit.kilometer
+
+def test_26():
+ assert UAS_IDS[0x26](b("0000")) == 0 * Unit.millivolt / Unit.millisecond
+ assert UAS_IDS[0x26](b("0001")) == 0.1 * Unit.millivolt / Unit.millisecond
+ assert UAS_IDS[0x26](b("FFFF")) == 6553.5 * Unit.millivolt / Unit.millisecond
+
+def test_27():
+ assert UAS_IDS[0x27](b("0000")) == 0 * Unit.grams_per_second
+ assert UAS_IDS[0x27](b("0001")) == 0.01 * Unit.grams_per_second
+ assert UAS_IDS[0x27](b("FFFF")) == 655.35 * Unit.grams_per_second
+
+def test_28():
+ assert UAS_IDS[0x28](b("0000")) == 0 * Unit.grams_per_second
+ assert UAS_IDS[0x28](b("0001")) == 1 * Unit.grams_per_second
+ assert UAS_IDS[0x28](b("FFFF")) == 65535 * Unit.grams_per_second
+
+def test_29():
+ assert UAS_IDS[0x29](b("0000")) == 0 * Unit.pascal / Unit.second
+ assert UAS_IDS[0x29](b("0004")) == 1 * Unit.pascal / Unit.second
+ assert UAS_IDS[0x29](b("FFFF")) == 16383.75 * Unit.pascal / Unit.second # deviates from standard examples
+
+def test_2A():
+ assert UAS_IDS[0x2A](b("0000")) == 0 * Unit.kilogram / Unit.hour
+ assert UAS_IDS[0x2A](b("0001")) == 0.001 * Unit.kilogram / Unit.hour
+ assert UAS_IDS[0x2A](b("FFFF")) == 65.535 * Unit.kilogram / Unit.hour
+
+def test_2B():
+ assert UAS_IDS[0x2B](b("0000")) == 0 * Unit.count
+ assert UAS_IDS[0x2B](b("0001")) == 1 * Unit.count
+ assert UAS_IDS[0x2B](b("FFFF")) == 65535 * Unit.count
+
+def test_2C():
+ assert UAS_IDS[0x2C](b("0000")) == 0 * Unit.gram
+ assert UAS_IDS[0x2C](b("0001")) == 0.01 * Unit.gram
+ assert UAS_IDS[0x2C](b("FFFF")) == 655.35 * Unit.gram
+
+def test_2D():
+ assert UAS_IDS[0x2D](b("0000")) == 0 * Unit.milligram
+ assert UAS_IDS[0x2D](b("0001")) == 0.01 * Unit.milligram
+ assert UAS_IDS[0x2D](b("FFFF")) == 655.35 * Unit.milligram
+
+def test_2E():
+ assert UAS_IDS[0x2E](b("0000")) == False
+ assert UAS_IDS[0x2E](b("0001")) == True
+ assert UAS_IDS[0x2E](b("FFFF")) == True
+
+def test_2F():
+ assert UAS_IDS[0x2F](b("0000")) == 0 * Unit.percent
+ assert UAS_IDS[0x2F](b("0001")) == 0.01 * Unit.percent
+ assert UAS_IDS[0x2F](b("2710")) == 100 * Unit.percent
+ assert UAS_IDS[0x2F](b("FFFF")) == 655.35 * Unit.percent
+
+def test_30():
+ assert float_equals(UAS_IDS[0x30](b("0000")), 0 * Unit.percent)
+ assert float_equals(UAS_IDS[0x30](b("0001")), 0.001526 * Unit.percent)
+ assert float_equals(UAS_IDS[0x30](b("FFFF")), 100.00641 * Unit.percent)
+
+def test_31():
+ assert UAS_IDS[0x31](b("0000")) == 0 * Unit.liter
+ assert UAS_IDS[0x31](b("0001")) == 0.001 * Unit.liter
+ assert UAS_IDS[0x31](b("FFFF")) == 65.535 * Unit.liter
+
+def test_32():
+ assert float_equals(UAS_IDS[0x32](b("0000")), 0 * Unit.inch)
+ assert float_equals(UAS_IDS[0x32](b("0010")), 0.0004883 * Unit.inch)
+ assert float_equals(UAS_IDS[0x32](b("0011")), 0.0005188 * Unit.inch)
+ assert float_equals(UAS_IDS[0x32](b("FFFF")), 1.9999695 * Unit.inch)
+
+def test_33():
+ assert float_equals(UAS_IDS[0x33](b("0000")), 0 * Unit.ratio)
+ assert float_equals(UAS_IDS[0x33](b("0001")), 0.00024414 * Unit.ratio)
+ assert float_equals(UAS_IDS[0x33](b("1000")), 1.0 * Unit.ratio)
+ assert float_equals(UAS_IDS[0x33](b("E5BE")), 14.36 * Unit.ratio)
+ assert float_equals(UAS_IDS[0x33](b("FFFF")), 16.0 * Unit.ratio)
+
+def test_34():
+ assert UAS_IDS[0x34](b("0000")) == 0 * Unit.minute
+ assert UAS_IDS[0x34](b("003C")) == 60 * Unit.minute
+ assert UAS_IDS[0x34](b("0E10")) == 3600 * Unit.minute
+ assert UAS_IDS[0x34](b("FFFF")) == 65535 * Unit.minute
+
+def test_35():
+ assert UAS_IDS[0x35](b("0000")) == 0 * Unit.millisecond
+ assert UAS_IDS[0x35](b("8000")) == 327680 * Unit.millisecond
+ assert UAS_IDS[0x35](b("EA60")) == 600000 * Unit.millisecond
+ assert UAS_IDS[0x35](b("FFFF")) == 655350 * Unit.millisecond
+
+def test_36():
+ assert UAS_IDS[0x36](b("0000")) == 0 * Unit.gram
+ assert UAS_IDS[0x36](b("0001")) == 0.01 * Unit.gram
+ assert UAS_IDS[0x36](b("FFFF")) == 655.35 * Unit.gram
+
+def test_37():
+ assert UAS_IDS[0x37](b("0000")) == 0 * Unit.gram
+ assert UAS_IDS[0x37](b("0001")) == 0.1 * Unit.gram
+ assert UAS_IDS[0x37](b("FFFF")) == 6553.5 * Unit.gram
+
+def test_38():
+ assert UAS_IDS[0x38](b("0000")) == 0 * Unit.gram
+ assert UAS_IDS[0x38](b("0001")) == 1 * Unit.gram
+ assert UAS_IDS[0x38](b("FFFF")) == 65535 * Unit.gram
+
+def test_39():
+ assert float_equals(UAS_IDS[0x39](b("0000")), -327.68 * Unit.percent)
+ assert float_equals(UAS_IDS[0x39](b("58F0")), -100 * Unit.percent)
+ assert float_equals(UAS_IDS[0x39](b("7FFF")), -0.01 * Unit.percent)
+ assert float_equals(UAS_IDS[0x39](b("8000")), 0 * Unit.percent)
+ assert float_equals(UAS_IDS[0x39](b("8001")), 0.01 * Unit.percent)
+ assert float_equals(UAS_IDS[0x39](b("A710")), 100 * Unit.percent)
+ assert float_equals(UAS_IDS[0x39](b("FFFF")), 327.67 * Unit.percent)
+
+def test_3A():
+ assert UAS_IDS[0x3A](b("0000")) == 0 * Unit.gram
+ assert UAS_IDS[0x3A](b("0001")) == 0.001 * Unit.gram
+ assert UAS_IDS[0x3A](b("FFFF")) == 65.535 * Unit.gram
+
+def test_3B():
+ assert float_equals(UAS_IDS[0x3B](b("0000")), 0 * Unit.gram)
+ assert float_equals(UAS_IDS[0x3B](b("0001")), 0.0001 * Unit.gram)
+ assert float_equals(UAS_IDS[0x3B](b("FFFF")), 6.5535 * Unit.gram)
+
+def test_3C():
+ assert UAS_IDS[0x3C](b("0000")) == 0 * Unit.microsecond
+ assert UAS_IDS[0x3C](b("8000")) == 3276.8 * Unit.microsecond
+ assert UAS_IDS[0x3C](b("EA60")) == 6000.0 * Unit.microsecond
+ assert UAS_IDS[0x3C](b("FFFF")) == 6553.5 * Unit.microsecond
+
+def test_3D():
+ assert UAS_IDS[0x3D](b("0000")) == 0 * Unit.milliampere
+ assert UAS_IDS[0x3D](b("0001")) == 0.01 * Unit.milliampere
+ assert UAS_IDS[0x3D](b("FFFF")) == 655.35 * Unit.milliampere
+
+def test_3E():
+ assert float_equals(UAS_IDS[0x3E](b("0000")), 0 * Unit.millimeter ** 2)
+ assert float_equals(UAS_IDS[0x3E](b("8000")), 1.9999 * Unit.millimeter ** 2)
+ assert float_equals(UAS_IDS[0x3E](b("FFFF")), 3.9999 * Unit.millimeter ** 2)
+
+def test_3F():
+ assert UAS_IDS[0x3F](b("0000")) == 0 * Unit.liter
+ assert UAS_IDS[0x3F](b("0001")) == 0.01 * Unit.liter
+ assert UAS_IDS[0x3F](b("FFFF")) == 655.35 * Unit.liter
+
+def test_40():
+ assert UAS_IDS[0x40](b("0000")) == 0 * Unit.ppm
+ assert UAS_IDS[0x40](b("0001")) == 1 * Unit.ppm
+ assert UAS_IDS[0x40](b("FFFF")) == 65535 * Unit.ppm
+
+def test_41():
+ assert UAS_IDS[0x41](b("0000")) == 0 * Unit.microampere
+ assert UAS_IDS[0x41](b("0001")) == 0.01 * Unit.microampere
+ assert UAS_IDS[0x41](b("FFFF")) == 655.35 * Unit.microampere
+
+
+
+
+"""
+signed Units
+"""
+
+def test_81():
+ assert UAS_IDS[0x81](b("8000")) == -32768 * Unit.count
+ assert UAS_IDS[0x81](b("FFFF")) == -1 * Unit.count
+ assert UAS_IDS[0x81](b("0000")) == 0 * Unit.count
+ assert UAS_IDS[0x81](b("0001")) == 1 * Unit.count
+ assert UAS_IDS[0x81](b("7FFF")) == 32767 * Unit.count
+
+def test_82():
+ assert UAS_IDS[0x82](b("8000")) == -3276.8 * Unit.count
+ assert UAS_IDS[0x82](b("FFFF")) == -0.1 * Unit.count
+ assert UAS_IDS[0x82](b("0000")) == 0 * Unit.count
+ assert UAS_IDS[0x82](b("0001")) == 0.1 * Unit.count
+ assert float_equals(UAS_IDS[0x82](b("7FFF")), 3276.7 * Unit.count)
+
+def test_83():
+ assert UAS_IDS[0x83](b("8000")) == -327.68 * Unit.count
+ assert UAS_IDS[0x83](b("FFFF")) == -0.01 * Unit.count
+ assert UAS_IDS[0x83](b("0000")) == 0 * Unit.count
+ assert UAS_IDS[0x83](b("0001")) == 0.01 * Unit.count
+ assert float_equals(UAS_IDS[0x83](b("7FFF")), 327.67 * Unit.count)
+
+def test_84():
+ assert UAS_IDS[0x84](b("8000")) == -32.768 * Unit.count
+ assert UAS_IDS[0x84](b("FFFF")) == -0.001 * Unit.count
+ assert UAS_IDS[0x84](b("0000")) == 0 * Unit.count
+ assert UAS_IDS[0x84](b("0001")) == 0.001 * Unit.count
+ assert float_equals(UAS_IDS[0x84](b("7FFF")), 32.767 * Unit.count)
+
+def test_85():
+ assert float_equals(UAS_IDS[0x85](b("8000")), -0.9999995 * Unit.count)
+ assert float_equals(UAS_IDS[0x85](b("FFFF")), -0.0000305 * Unit.count)
+ assert float_equals(UAS_IDS[0x85](b("0000")), 0 * Unit.count)
+ assert float_equals(UAS_IDS[0x85](b("0001")), 0.0000305 * Unit.count)
+ assert float_equals(UAS_IDS[0x85](b("7FFF")), 0.9999995 * Unit.count)
+
+def test_86():
+ assert float_equals(UAS_IDS[0x86](b("8000")), -9.999995 * Unit.count)
+ assert float_equals(UAS_IDS[0x86](b("FFFF")), -0.000305 * Unit.count)
+ assert float_equals(UAS_IDS[0x86](b("0000")), 0 * Unit.count)
+ assert float_equals(UAS_IDS[0x86](b("0001")), 0.000305 * Unit.count)
+ assert float_equals(UAS_IDS[0x86](b("7FFF")), 9.999995 * Unit.count)
+
+def test_87():
+ assert UAS_IDS[0x87](b("8000")) == -32768 * Unit.ppm
+ assert UAS_IDS[0x87](b("FFFF")) == -1 * Unit.ppm
+ assert UAS_IDS[0x87](b("0000")) == 0 * Unit.ppm
+ assert UAS_IDS[0x87](b("0001")) == 1 * Unit.ppm
+ assert UAS_IDS[0x87](b("7FFF")) == 32767 * Unit.ppm
+
+def test_8A():
+ # the standard gives example values that don't line up perfectly
+ # with the scale. The last two tests here deviate from the standard
+ assert float_equals(UAS_IDS[0x8A](b("8000")), -3997.696 * Unit.millivolt) # -3999.998 mV
+ assert float_equals(UAS_IDS[0x8A](b("FFFF")), -0.122 * Unit.millivolt)
+ assert float_equals(UAS_IDS[0x8A](b("0000")), 0 * Unit.millivolt)
+ assert float_equals(UAS_IDS[0x8A](b("0001")), 0.122 * Unit.millivolt)
+ assert float_equals(UAS_IDS[0x8A](b("7FFF")), 3997.574 * Unit.millivolt) # 3999.876 mV
+
+def test_8B():
+ assert UAS_IDS[0x8B](b("8000")) == -32.768 * Unit.volt
+ assert UAS_IDS[0x8B](b("FFFF")) == -0.001 * Unit.volt
+ assert UAS_IDS[0x8B](b("0000")) == 0 * Unit.volt
+ assert UAS_IDS[0x8B](b("0001")) == 0.001 * Unit.volt
+ assert UAS_IDS[0x8B](b("7FFF")) == 32.767 * Unit.volt
+
+def test_8C():
+ assert UAS_IDS[0x8C](b("8000")) == -327.68 * Unit.volt
+ assert UAS_IDS[0x8C](b("FFFF")) == -0.01 * Unit.volt
+ assert UAS_IDS[0x8C](b("0000")) == 0 * Unit.volt
+ assert UAS_IDS[0x8C](b("0001")) == 0.01 * Unit.volt
+ assert UAS_IDS[0x8C](b("7FFF")) == 327.67 * Unit.volt
+
+def test_8D():
+ assert float_equals(UAS_IDS[0x8D](b("8000")), -128 * Unit.milliampere)
+ assert float_equals(UAS_IDS[0x8D](b("FFFF")), -0.00390625 * Unit.milliampere)
+ assert float_equals(UAS_IDS[0x8D](b("0000")), 0 * Unit.milliampere)
+ assert float_equals(UAS_IDS[0x8D](b("0001")), 0.00390625 * Unit.milliampere)
+ assert float_equals(UAS_IDS[0x8D](b("7FFF")), 127.996 * Unit.milliampere)
+
+def test_8E():
+ assert UAS_IDS[0x8E](b("8000")) == -32.768 * Unit.ampere
+ assert UAS_IDS[0x8E](b("FFFF")) == -0.001 * Unit.ampere
+ assert UAS_IDS[0x8E](b("0000")) == 0 * Unit.ampere
+ assert UAS_IDS[0x8E](b("0001")) == 0.001 * Unit.ampere
+ assert UAS_IDS[0x8E](b("7FFF")) == 32.767 * Unit.ampere
+
+def test_90():
+ assert UAS_IDS[0x90](b("8000")) == -32768 * Unit.millisecond
+ assert UAS_IDS[0x90](b("FFFF")) == -1 * Unit.millisecond
+ assert UAS_IDS[0x90](b("0000")) == 0 * Unit.millisecond
+ assert UAS_IDS[0x90](b("0001")) == 1 * Unit.millisecond
+ assert UAS_IDS[0x90](b("7FFF")) == 32767 * Unit.millisecond
+
+def test_96():
+ assert float_equals(UAS_IDS[0x96](b("8000")), Unit.Quantity(-3276.8, Unit.celsius))
+ assert float_equals(UAS_IDS[0x96](b("FFFF")), Unit.Quantity(-0.1, Unit.celsius))
+ assert float_equals(UAS_IDS[0x96](b("0000")), Unit.Quantity(0, Unit.celsius))
+ assert float_equals(UAS_IDS[0x96](b("0001")), Unit.Quantity(0.1, Unit.celsius))
+ assert float_equals(UAS_IDS[0x96](b("7FFF")), Unit.Quantity(3276.7, Unit.celsius))
+
+def test_99():
+ assert float_equals(UAS_IDS[0x99](b("8000")), -3276.8 * Unit.kilopascal)
+ assert float_equals(UAS_IDS[0x99](b("FFFF")), -0.1 * Unit.kilopascal)
+ assert float_equals(UAS_IDS[0x99](b("0000")), 0 * Unit.kilopascal)
+ assert float_equals(UAS_IDS[0x99](b("0001")), 0.1 * Unit.kilopascal)
+ assert float_equals(UAS_IDS[0x99](b("7FFF")), 3276.7 * Unit.kilopascal)
+
+def test_9C():
+ assert UAS_IDS[0x9C](b("8000")) == -327.68 * Unit.degree
+ assert UAS_IDS[0x9C](b("FFFF")) == -0.01 * Unit.degree
+ assert UAS_IDS[0x9C](b("0000")) == 0 * Unit.degree
+ assert UAS_IDS[0x9C](b("0001")) == 0.01 * Unit.degree
+ assert UAS_IDS[0x9C](b("7FFF")) == 327.67 * Unit.degree
+
+def test_9D():
+ assert UAS_IDS[0x9D](b("8000")) == -16384 * Unit.degree
+ assert UAS_IDS[0x9D](b("FFFF")) == -0.5 * Unit.degree
+ assert UAS_IDS[0x9D](b("0000")) == 0 * Unit.degree
+ assert UAS_IDS[0x9D](b("0001")) == 0.5 * Unit.degree
+ assert UAS_IDS[0x9D](b("7FFF")) == 16383.5 * Unit.degree
+
+def test_A8():
+ assert UAS_IDS[0xA8](b("8000")) == -32768 * Unit.grams_per_second
+ assert UAS_IDS[0xA8](b("FFFF")) == -1 * Unit.grams_per_second
+ assert UAS_IDS[0xA8](b("0000")) == 0 * Unit.grams_per_second
+ assert UAS_IDS[0xA8](b("0001")) == 1 * Unit.grams_per_second
+ assert UAS_IDS[0xA8](b("7FFF")) == 32767 * Unit.grams_per_second
+
+def test_A9():
+ assert UAS_IDS[0xA9](b("8000")) == -8192 * Unit.pascal / Unit.second
+ assert UAS_IDS[0xA9](b("FFFC")) == -1 * Unit.pascal / Unit.second
+ assert UAS_IDS[0xA9](b("0000")) == 0 * Unit.pascal / Unit.second
+ assert UAS_IDS[0xA9](b("0004")) == 1 * Unit.pascal / Unit.second
+ assert UAS_IDS[0xA9](b("7FFF")) == 8191.75 * Unit.pascal / Unit.second
+
+def test_AD():
+ assert UAS_IDS[0xAD](b("8000")) == -327.68 * Unit.milligram
+ assert UAS_IDS[0xAD](b("FFFF")) == -0.01 * Unit.milligram
+ assert UAS_IDS[0xAD](b("0000")) == 0 * Unit.milligram
+ assert UAS_IDS[0xAD](b("0001")) == 0.01 * Unit.milligram
+ assert UAS_IDS[0xAD](b("7FFF")) == 327.67 * Unit.milligram
+
+def test_AE():
+ assert UAS_IDS[0xAE](b("8000")) == -3276.8 * Unit.milligram
+ assert UAS_IDS[0xAE](b("FFFF")) == -0.1 * Unit.milligram
+ assert UAS_IDS[0xAE](b("0000")) == 0 * Unit.milligram
+ assert UAS_IDS[0xAE](b("0001")) == 0.1 * Unit.milligram
+ assert float_equals(UAS_IDS[0xAE](b("7FFF")), 3276.7 * Unit.milligram)
+
+def test_AF():
+ assert UAS_IDS[0xAF](b("8000")) == -327.68 * Unit.percent
+ assert UAS_IDS[0xAF](b("FFFF")) == -0.01 * Unit.percent
+ assert UAS_IDS[0xAF](b("0000")) == 0 * Unit.percent
+ assert UAS_IDS[0xAF](b("0001")) == 0.01 * Unit.percent
+ assert UAS_IDS[0xAF](b("7FFF")) == 327.67 * Unit.percent
+
+def test_B0():
+ assert UAS_IDS[0xB0](b("8000")) == -100.007936 * Unit.percent
+ assert UAS_IDS[0xB0](b("FFFF")) == -0.003052 * Unit.percent
+ assert UAS_IDS[0xB0](b("0000")) == 0 * Unit.percent
+ assert UAS_IDS[0xB0](b("0001")) == 0.003052 * Unit.percent
+ assert UAS_IDS[0xB0](b("7FFF")) == 100.004884 * Unit.percent
+
+def test_B1():
+ assert UAS_IDS[0xB1](b("8000")) == -65536 * Unit.millivolt / Unit.second
+ assert UAS_IDS[0xB1](b("FFFF")) == -2 * Unit.millivolt / Unit.second
+ assert UAS_IDS[0xB1](b("0000")) == 0 * Unit.millivolt / Unit.second
+ assert UAS_IDS[0xB1](b("0001")) == 2 * Unit.millivolt / Unit.second
+ assert UAS_IDS[0xB1](b("7FFF")) == 65534 * Unit.millivolt / Unit.second
+
+def test_FC():
+ assert UAS_IDS[0xFC](b("8000")) == -327.68 * Unit.kilopascal
+ assert UAS_IDS[0xFC](b("FFFF")) == -0.01 * Unit.kilopascal
+ assert UAS_IDS[0xFC](b("0000")) == 0 * Unit.kilopascal
+ assert UAS_IDS[0xFC](b("0001")) == 0.01 * Unit.kilopascal
+ assert UAS_IDS[0xFC](b("7FFF")) == 327.67 * Unit.kilopascal
+
+def test_FD():
+ assert UAS_IDS[0xFD](b("8000")) == -32.768 * Unit.kilopascal
+ assert UAS_IDS[0xFD](b("FFFF")) == -0.001 * Unit.kilopascal
+ assert UAS_IDS[0xFD](b("0000")) == 0 * Unit.kilopascal
+ assert UAS_IDS[0xFD](b("0001")) == 0.001 * Unit.kilopascal
+ assert UAS_IDS[0xFD](b("7FFF")) == 32.767 * Unit.kilopascal
+
+def test_FE():
+ assert UAS_IDS[0xFE](b("8000")) == -8192 * Unit.pascal
+ assert UAS_IDS[0xFE](b("FFFC")) == -1 * Unit.pascal
+ assert UAS_IDS[0xFE](b("0000")) == 0 * Unit.pascal
+ assert UAS_IDS[0xFE](b("0004")) == 1 * Unit.pascal
+ assert UAS_IDS[0xFE](b("7FFF")) == 8191.75 * Unit.pascal