├── .gitignore ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── bin ├── Install_telemetry-obd.sh ├── obd_logger.sh └── obd_tester.sh ├── config └── default.ini ├── docs ├── BT-connections.jpg ├── Python310-Install.md ├── Python311-Install.md ├── README-BluetoothPairing.md ├── README-DriveSequence.JPG ├── README-HighLevelSystemView.JPG ├── README-RunCycles.JPG ├── README-TestEnvironment.JPG ├── README-UltraDict.md ├── README-rpi-gui-bt-devices-authenticated.jpg ├── README-rpi-gui-bt-devices-connect-failed.jpg ├── README-rpi-gui-bt-devices-connect.jpg ├── README-rpi-gui-bt-devices-dialog.jpg ├── README-rpi-gui-bt-devices-search-pair.jpg ├── README-rpi-gui-bt-devices-search-pairing-authentication.jpg ├── README-rpi-gui-bt-devices-search-pairing-request.jpg ├── README-rpi-gui-bt-devices-search.jpg ├── README-rpi-gui-bt-devices-setup.jpg ├── README-rpi-gui-bt-devices-trust.jpg ├── README-rpi-gui-bt-devices-trusted.jpg ├── README-rpi-gui-bt-devices.jpg ├── README-rpi-gui-bt.jpg └── python-OBD-Install.md ├── etc └── rc.local ├── pyproject.toml ├── root └── bin │ └── telemetry.rc.local.obd ├── setup.cfg └── telemetry_obd ├── __init__.py ├── add_commands.py ├── list_all_commands.py ├── obd_command_tester.py ├── obd_common_functions.py └── obd_logger.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | config/ 24 | DataAnalysis/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | *.swp 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # other junk 135 | stuff/ 136 | data/ 137 | tmp/ 138 | examples/ 139 | docs/ 140 | *.json 141 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020, 2021, 2022, 2023 Larry Pearson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft telemetry_obd/*.py 2 | graft bin 3 | graft etc 4 | graft config/default.ini 5 | global-exclude *.py[co] 6 | prune .mypy_cache 7 | prune .pytest_cache 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telemetry OBD Logger 2 | 3 | ## **NOTICE: REPOSITORIES INTEGRATED INTO SINGLE REPOSITORY** 4 | 5 | This repository along with other related repositories have been integrated into [**Vehicle Telemetry System**, a system for collecting and processing motor vehicle data using included sensor modules.](https://github.com/thatlarrypearson/vehicle-telemetry-system). 6 | 7 | The Telemetry OBD Logger captures vehicle performance data using an OBD interface device attached to the vehicle. While the logger is running, it writes output to files. Data from multiple vehicles can easily be logged. Data from each different vehicle is stored in a directory/folder matching the vehicle's VIN or vehicle identification number. 8 | 9 | The software is designed to run on Raspberry Pi with Raspberry Pi OS (formerly known as Raspbian) installed. Bluetooth capabilities are added to the Raspberry Pi through a USB Bluetooth adapter (BT Dongle) and installed software (Bluetooth Driver and tools). 10 | 11 | The OBD Logger software runs on Python versions 3.11 (optionally 3.10). 12 | 13 | ![High Level System View](docs/README-HighLevelSystemView.JPG) 14 | 15 | ## OBD Logger 16 | 17 | ### Command Line Usage 18 | 19 | The Telemetry OBD Logger application command line interface (CLI) is as follows: 20 | 21 | ```bash 22 | $ python3.11 -m telemetry_obd.obd_logger --help 23 | usage: obd_logger.py [-h] [--config_file CONFIG_FILE] [--config_dir CONFIG_DIR] [--full_cycles FULL_CYCLES] [--timeout TIMEOUT] [--logging] [--no_fast] 24 | [--verbose] 25 | [--version] 26 | [base_path] 27 | 28 | Telemetry OBD Logger 29 | 30 | positional arguments: 31 | base_path Relative or absolute output data directory. Defaults to 'data'. 32 | 33 | options: 34 | -h, --help show this help message and exit 35 | --config_file CONFIG_FILE 36 | Settings file name. Defaults to '.ini' or 'default.ini'. 37 | --config_dir CONFIG_DIR 38 | Settings directory path. Defaults to './config'. 39 | --full_cycles FULL_CYCLES 40 | The number of full cycles before a new output file is started. Default is 50. 41 | --timeout TIMEOUT The number seconds before the current command times out. Default is 1.0 seconds. 42 | --logging Turn on logging in python-obd library. Default is off. 43 | --no_fast When on, commands for every request will be unaltered with potentially long timeouts when the car doesn't respond promptly or at 44 | all. When off (fast is on), commands are optimized before being sent to the car. A timeout is added at the end of the command. 45 | Default is off. 46 | --output_file_name_counter 47 | Base output file name on counter not timestamps 48 | --verbose Turn verbose output on. Default is off. 49 | --version Print version number and exit. 50 | $ 51 | ``` 52 | 53 | #### ```--timeout TIMEOUT``` 54 | 55 | The timeout value determines how long a read request can take between the underlying ```python-OBD``` library and the OBD reader device. If one or more individual commands are causing problems by intermittently responding with ```"no response"``` instead of a real value, an increase in the ```timeout``` value may help alleviate the problem. 56 | 57 | #### ```--no_fast``` 58 | 59 | ```--no_fast``` can also be used to reduce the number of ```"no response"```s but be aware of the consequences. For commands that are not available on the vehicle being instrumented, the software may just wait forever for a response that will never come. 60 | 61 | #### ```--version``` 62 | 63 | Responds with the version and exits. 64 | 65 | #### Telemetry OBD Logger Run Cycles 66 | 67 | While logging, OBD Logger submits a pattern of OBD commands to the vehicle and stores the vehicle's responses. There are three patterns: 68 | 69 | - Startup 70 | - Housekeeping 71 | - Cycle 72 | 73 | ![Run Cycles](docs/README-RunCycles.JPG) 74 | 75 | ##### Startup 76 | 77 | The startup list of OBD commands is only executed when the program starts up. Typically, this list of OBD commands includes: 78 | 79 | - OBD commands whose return values never change (e.g. ```ECU_NAME```, ```ELM_VERSION```, ```ELM_VOLTAGE```) 80 | - OBD commands with slow changing return values that might be needed for startup baseline like ```AMBIANT_AIR_TEMP``` and ```BAROMETRIC_PRESSURE```. 81 | 82 | ##### Housekeeping 83 | 84 | A list of OBD commands that have ("relatively") "slow changing" return values such as ```AMBIANT_AIR_TEMP``` and ```BAROMETRIC_PRESSURE```. These are commands that need to be run over and over again but in a slower loop. 85 | 86 | ##### Cycle 87 | 88 | A list of OBD commands that have fast changing return values such as ```RPM```, ```MAF``` (Mass Air Flow) and ```PERCENT_TORQUE```. The idea is for these commands to run over and over again in relatively fast loops. 89 | 90 | ##### Full Cycle 91 | 92 | The repeating part of the OBD command pattern is called a "full cycle" and has OBD commands from Cycle executed in a group followed by the next Housekeeping command. This basic pattern repeats over and over. When the end of the Housekeeping commands is reached, a "Full Cycle" has been achieved. 93 | 94 | The total number of command submissions in a full cycle is the ```count of commands in Housekeeping``` times (one plus the ```count of commands in Cycle```). 95 | 96 | The ```--full_cycles``` parameter is used to set the number of ```full_cycles``` contained in output data files. Once the ```--full_cycles``` limit is reached, the data file is closed and a new one is opened. This keeps data loss from unplanned Raspberry Pi shutdowns to a minimum. 97 | 98 | #### Telemetry OBD Logger Configuration Files 99 | 100 | Configuration files are used to tell OBD Logger what OBD commands to send the vehicle and the order to send those commands in. A sample configuration file is shown below and another one is included in the source code. 101 | 102 | ##### Default Configuration File 103 | 104 | A default configuration file is included in the repository at ```config/default.ini```. This configuration file contains most OBD commands. There are wide variations in supported command sets by manufacturer, model, trim level and year. By starting out with this configuration file, OBD Logger will try all commands. After a full cycle is run, unsupported commands will respond with ```"obd_response_value": "no response"``` in the output data. 105 | 106 | Some commands will result in an OBD response value of ```"no response"``` (```"obd_response_value": "no response"```) when the vehicle is unable to satisfy the OBD data request quickly enough. You can identify this problem by searching for all responses for a particular command and seeing if sometimes the command responds with ```"no response"``` or with a value. 107 | 108 | For example, 2017 Ford F-450 truck ```FUEL_RATE``` command in the ```cycle``` section of the configuration file returned mixed results. In 1,124 attempts, 1084 responded with a good value while 40 responded with ```no response```. 109 | 110 | ```bash 111 | human@computer:data/FT8W4DT5HED00000$ grep FUEL_RATE FT8W4DT5HED00000-20210910204443-utc.json | grep "no response" | wc -l 112 | 40 113 | human@computer:data/FT8W4DT5HED00000$ grep FUEL_RATE FT8W4DT5HED00000-20210910204443-utc.json | grep -v "no response" | wc -l 114 | 1084 115 | ``` 116 | 117 | This problem may be solved by increasing the OBD command timeout from its default to a higher value. Use the ```--timeout``` setting when invoking the ```obd_logger``` command. 118 | 119 | ### Telemetry OBD Logger Output Data Files 120 | 121 | Output data files are in a hybrid format. Data files contain records separated by line feeds (```LF```) or carriage return and line feeds (```CF``` and ```LF```). The records themselves are formatted in JSON. Sample output follows: 122 | 123 | ```json 124 | {"command_name": "AMBIANT_AIR_TEMP", "obd_response_value": "25 degC", "iso_ts_pre": "2020-09-09T15:38:29.114895+00:00", "iso_ts_post": "2020-09-09T15:38:29.185457+00:00"} 125 | {"command_name": "BAROMETRIC_PRESSURE", "obd_response_value": "101 kilopascal", "iso_ts_pre": "2020-09-09T15:38:29.186497+00:00", "iso_ts_post": "2020-09-09T15:38:29.259106+00:00"} 126 | {"command_name": "CONTROL_MODULE_VOLTAGE", "obd_response_value": "0.0 volt", "iso_ts_pre": "2020-09-09T15:38:29.260143+00:00", "iso_ts_post": "2020-09-09T15:38:29.333047+00:00"} 127 | {"command_name": "VIN", "obd_response_value": "TEST_VIN_22_CHARS", "iso_ts_pre": "2020-09-09T15:38:30.029478+00:00", "iso_ts_post": "2020-09-09T15:38:30.061014+00:00"} 128 | {"command_name": "FUEL_STATUS", "obd_response_value": "no response", "iso_ts_pre": "2020-09-09T15:38:29.771997+00:00", "iso_ts_post": "2020-09-09T15:38:29.824129+00:00"} 129 | ``` 130 | 131 | #### JSON Fields 132 | 133 | - ```command_name``` 134 | OBD command name submitted to vehicle. 135 | 136 | - ```obd_response_value``` 137 | OBD response value returned by the vehicle. When the OBD command gets no response, the response is ```"no response"```. Response values are either a string like ```"no response"``` and ```"TEST_VIN_22_CHARS"``` or they are a [Pint](https://pint.readthedocs.io/en/stable/) encoded value like ```"25 degC"``` and ```"101 kilopascal"```. 138 | 139 | Some OBD commands will respond with multiple values in a list. The values within the list can also be Pint values. This works just fine in JSON but the code reading these output files will need to be able to manage embedded lists within the response values. [Telemetry OBD Data To CSV File](https://github.com/thatlarrypearson/telemetry-obd-log-to-csv) contains two programs, ```obd_log_evaluation``` and ```obd_log_to_csv```, providing good examples of how to handle multiple return values. 140 | 141 | - ```iso_ts_pre``` 142 | ISO formatted timestamp taken before the OBD command was issued to the vehicle (```datetime.isoformat(datetime.now(tz=timezone.utc))```). 143 | 144 | - ```iso_ts_post``` 145 | ISO formatted timestamp taken after the OBD command was issued to the vehicle (```datetime.isoformat(datetime.now(tz=timezone.utc))```). 146 | 147 | [Pint](https://pint.readthedocs.io/en/stable/) encoded values are strings with a numeric part followed by the unit. For example, ```"25 degC"``` represents 25 degrees Centigrade. ```"101 kilopascal"``` is around 14.6 PSI (pounds per square inch). Pint values are used so that the units are always kept with the data and so that unit conversions can easily be done in downstream analysis software. These strings are easy to deserialize to Pint objects for use in Python programs. 148 | 149 | ### Telemetry OBD Logger Debug Output 150 | 151 | OBD Logger provides additional information while running when the ```--verbose``` option is used. Additionally, The underlying python ```obd``` library (```python-obd```) supports detailed low-level logging capabilities which can be enabled within OBD Logger with the ```--logging``` option. 152 | 153 | Sample ```--logging``` output follows: 154 | 155 | ```text 156 | [obd.obd] ======================= python-OBD (v0.7.1) ======================= 157 | INFO:obd.obd:======================= python-OBD (v0.7.1) ======================= 158 | [obd.obd] Using scan_serial to select port 159 | INFO:obd.obd:Using scan_serial to select port 160 | [obd.obd] Available ports: ['/dev/rfcomm0'] 161 | INFO:obd.obd:Available ports: ['/dev/rfcomm0'] 162 | [obd.obd] Attempting to use port: /dev/rfcomm0 163 | INFO:obd.obd:Attempting to use port: /dev/rfcomm0 164 | [obd.elm327] Initializing ELM327: PORT=/dev/rfcomm0 BAUD=auto PROTOCOL=auto 165 | INFO:obd.elm327:Initializing ELM327: PORT=/dev/rfcomm0 BAUD=auto PROTOCOL=auto 166 | [obd.elm327] Response from baud 38400: b'\x7f\x7f\r?\r\r>' 167 | DEBUG:obd.elm327:Response from baud 38400: b'\x7f\x7f\r?\r\r>' 168 | [obd.elm327] Choosing baud 38400 169 | DEBUG:obd.elm327:Choosing baud 38400 170 | [obd.elm327] write: b'ATZ\r' 171 | DEBUG:obd.elm327:write: b'ATZ\r' 172 | [obd.elm327] wait: 1 seconds 173 | DEBUG:obd.elm327:wait: 1 seconds 174 | [obd.elm327] read: b'ATZ\r\r\rELM327 v1.5\r\r>' 175 | DEBUG:obd.elm327:read: b'ATZ\r\r\rELM327 v1.5\r\r>' 176 | [obd.elm327] write: b'ATE0\r' 177 | DEBUG:obd.elm327:write: b'ATE0\r' 178 | [obd.elm327] read: b'ATE0\rOK\r\r' 179 | DEBUG:obd.elm327:read: b'ATE0\rOK\r\r' 180 | [obd.elm327] write: b'ATH1\r' 181 | DEBUG:obd.elm327:write: b'ATH1\r' 182 | [obd.elm327] read: b'OK\r\r>' 183 | DEBUG:obd.elm327:read: b'OK\r\r>' 184 | [obd.elm327] write: b'ATL0\r' 185 | ``` 186 | 187 | ### OBD Logger Data File Naming Convention 188 | 189 | See [Telemetry System Boot and Application Startup Counter](https://github.com/thatlarrypearson/telemetry-counter). 190 | 191 | ## Testing All Available OBD Commands To See Which Ones Work On Your Vehicle 192 | 193 | ```Telemetry OBD Command Tester``` can be used to determine which set of OBD commands are supported by a given vehicle. 194 | 195 | ```bash 196 | $ python3.11 -m telemetry_obd.obd_command_tester --help 197 | usage: obd_command_tester.py [-h] [--base_path BASE_PATH] [--cycles CYCLES] [--timeout TIMEOUT] [--logging] [--no_fast] [--verbose] 198 | 199 | Telemetry OBD Command Tester 200 | 201 | optional arguments: 202 | -h, --help show this help message and exit 203 | --base_path BASE_PATH 204 | Relative or absolute output data directory. Defaults to 'data'. 205 | --cycles CYCLES The number of cycles before ending. A cycle consists of all known OBD commands. Default is 10. 206 | --timeout TIMEOUT The number seconds before a command times out. Default is 0.5 seconds. 207 | --logging Turn on logging in python-obd library. Default is off. 208 | --no_fast When on, commands for every request will be unaltered with potentially long timeouts when the car 209 | doesn't respond promptly or at all. When off (fast is on), commands are optimized before being 210 | sent to the car. A timeout is added at the end of the command. Default is off so fast is on. 211 | --output_file_name_counter 212 | Base output file name on counter not timestamps 213 | --verbose Turn verbose output on. Default is off. 214 | ``` 215 | 216 | The output file format is the same as ```telemetry_obd.obd_logger``` as are many of the command line arguments. ```--cycles``` determines the number of times the full list of known OBD commands is tried. The OBD command list needs to be run a number of times because vehicles don't alway respond to requests in a timely manner. 217 | 218 | Test output files are named differently than ```obd_logger``` data files. Both test and ```obd_logger``` data files will be placed in the ```{BASE_PATH}/{VIN}``` directory. For example, using ```data``` (default) as the base path, if the vehicle VIN is ```FT8W4DT5HED00000```, then test files will be of the form ```data/FT8W4DT5HED00000/FT8W4DT5HED00000-TEST-20211012141917-utc.json```. The ```obd_logger``` files will be of the form ```data/FT8W4DT5HED00000/FT8W4DT5HED00000-20211012141917-utc.json```. 219 | 220 | ## Issues Surfaced During Vehicle Testing 221 | 222 | *IMPORTANT!* Some vehicle identification numbers (VIN) are known to drop characters when being recorded. Not to worry as the recorded VIN is still *VERY* *UNLIKELY* to overlap with another vehicle's VIN (unless you are working with thousands of vehicles). VIN data is provided by the ```python-obd``` library. 223 | 224 | * 2017 Ford F-450 VIN is missing leading ```1``` digit/letter 225 | * ```1FT8W4DT5HED00000``` is recorded as ```FT8W4DT5HED00000``` 226 | * 2013 Jeep Wrangler Rubicon VIN is missing leading ```1``` and trailing 2 digits/letters 227 | * ```1C4HJWCG5DL500000``` is recorded as ```C4HJWCG5DL5000``` 228 | * 2021 Toyota Sienna Hybrid LE is missing the trailing ```0``` digit/letter 229 | * ```5TDKRKEC3MS042260``` is recorded as ```5TDKRKEC3MS04226``` 230 | 231 | *IMPORTANT!* Calibration Verification Numbers (CVN) come in 4 byte sets. Multiple CVN (more than 4) can be returned through a single CVN command. As a result, the CVN data isn't likely to be valid as provided by the ```python-obd``` library. 232 | 233 | *IMPORTANT!* Calibration IDs (CALIBRATION_ID) come in 16 byte sets. Multiple CALIBRATION_ID (more than 4) can be returned through a single CALIBRATION_ID command. As a result, the CALIBRATION_ID data isn't likely to be valid as provided by the ```python-obd``` library. 234 | 235 | Problems like the above can be found by running ```telemetry_obd.obd_logger``` and ```telemetry_obd.obd_command_tester``` with the ```--logging``` and ```--verbose``` command line arguments. Look for "```DEBUG:obd.OBDCommand:Message was longer than expected```" in the output. 236 | 237 | ## Configuration File Creation and Validation 238 | 239 | When a VIN (vehicle identification number) specific configuration file doesn't exist, the OBD Logger program defaults to using the ```"default.ini"``` configuration file. This file, included in the software distribution under ```"config/default.ini"``` contains most known OBD commands. Because of the wide variations in supported command sets by manufacturer, model, trim level and year made, it is difficult to know what OBD commands a specific car will respond to. Additionally, manufacturers don't typically publish lists of valid OBD commands for each vehicle sold. This "try-them-all" method seems to be the only approach to identifying which OBD commands a specific vehicle will respond to. 240 | 241 | The preferred way to *"try-them-all"*, that is try every known OBD command, is to use the ```telemetry_obd.obd_command_tester``` program. Once all the possible known OBD commands have been tried, it becomes possible to create a list of valid known commands to be used in the creation of a vehicle specific configuration file. The OBD Logger software was written to automatically choose configuration files appropriately named ```".ini"``` by default. If the ```".ini"``` isn't available, then the other default, ```"default.ini"```, is chosen. 242 | 243 | Analysis of ```telemetry_obd.obd_command_tester``` and ```telemetry_obd.obd_logger``` output is done by ```telemetry_obd_log_to_csv.obd_log_evaluation``` found in the [Telemetry OBD Data To CSV File](https://github.com/thatlarrypearson/telemetry-obd-log-to-csv) repository. 244 | 245 | When creating vehicle specific configuration files, use ```obd_log_evaluation``` to determine the list of commands providing valid vehicle responses. Only valid OBD commands should be used long term when gathering vehicle data. 246 | 247 | ## Raspberry Pi System Installation 248 | 249 | The recommended Raspberry Pi system is a [Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/), [Raspberry Pi 400](https://www.raspberrypi.com/products/raspberry-pi-400/) or Raspberry Pi 5 with 4 GB RAM or more. The full 64 bit release of [Raspberry Pi OS](https://www.raspberrypi.com/software/) version 12 (bookworm) or newer, including the GUI is the recommended operating system. When choosing a [Micro-SD card](https://en.wikipedia.org/wiki/SD_card) for storage, look for Internet resources like [Best microSD Cards for Raspberry Pi & SBCs](https://bret.dk/best-raspberry-pi-micro-sd-cards/) as a guide to making an appropriate selection. Select cards 32 GB or larger. 250 | 251 | After installing Raspberry Pi OS on a Raspberry Pi 4 computer, update the operating system to the newest version. One way to do this is as follows: 252 | 253 | ```bash 254 | # update and upgrade Linux/Raspberry Pi OS 255 | sudo apt-get update 256 | sudo apt-get upgrade -y 257 | sudo apt-get autoremove -y 258 | sudo shutdown -r now 259 | sudo apt-get dist-upgrade -y 260 | sudo shutdown -r now 261 | ``` 262 | 263 | Install useful software: 264 | 265 | ```bash 266 | # git software 267 | sudo apt-get install -y git 268 | ``` 269 | 270 | ### Bluetooth Software 271 | 272 | Install the Bluetooth support software and then reboot the system: 273 | 274 | ```bash 275 | # Bluetooth support software 276 | sudo apt-get install -y bluetooth bluez bluez-tools blueman bluez-hcidump 277 | # Bluetooth support software requires reboot to become activated 278 | sudo shutdown -r now 279 | ``` 280 | 281 | ### Pairing Bluetooth OBD Devices 282 | 283 | Bluetooth OBD adapters must be *paired* and *trusted* before they can be used. The *pairing* and *trust* process is covered in [Pairing Bluetooth OBD Devices](./docs/README-BluetoothPairing.md). 284 | 285 | ### Bluetooth Trouble 286 | 287 | After operating system upgrades, Bluetooth may not operate as expected. Possible solutions may include 288 | 289 | - Completely un-pair/remove OBD adapter from Bluetooth configuration followed by a reboot. Next, boot the system and follow the [Bluetooth pairing instructions](./docs/README-BluetoothPairing.md). 290 | - Follow instructions in [Use app to connect to pi via bluetooth](https://forums.raspberrypi.com/viewtopic.php?p=947185#p947185). 291 | 292 | ### Python 3.11 293 | 294 | Validate that your Raspberry Pi has Python version 3.11 available: 295 | 296 | ```bash 297 | # Python 3 version 298 | human@hostname:~$ python3 --version 299 | Python 3.6.9 300 | # Python 3.11 version 301 | human@hostname:~$ python3.11 --version 302 | Python 3.11.5 303 | human@hostname:~$ 304 | ``` 305 | 306 | The latest Raspberry Pi OS version ```Bookworm (12.2)```, ships with Python 3.11 but don't use it. It has a new feature that will prevent you from using ```pip``` to download necessary Python packages. Instead, download/make/install Python 3.11 from source following the instructions below. 307 | 308 | ```bash 309 | # Python 3 version on Rasberry Pi OS Bookworm Version 12.2 310 | human@hostname:~$ python3 --version 311 | Python 3.11.2 312 | human@hostname:~$ 313 | ``` 314 | 315 | If *Python 3.11*, isn't already installed you will need to compile it from source before installing it. Follow the [Python 3.11 Install Instructions](docs/Python311-Install.md) to download, compile and install the preferred Python 3.11. 316 | 317 | Alternatively, you may follow the [Python 3.10 Install Instructions](docs/Python310-Install.md) to download, compile and install Python 3.10. 318 | 319 | ### Installing ```telemetry_obd``` Package 320 | 321 | ```bash 322 | # get latest version of this software from github repository 323 | git clone https://github.com/thatlarrypearson/telemetry-obd.git 324 | cd telemetry-obd 325 | python3.11 -m build 326 | python3.11 -m pip install --user dist/telemetry_obd-0.4.2-py3-none-any.whl 327 | 328 | # make shell programs executable 329 | chmod 0755 bin/*.sh 330 | cd 331 | ``` 332 | 333 | On Windows 10, connecting to USB or Bluetooth ELM 327 OBD interfaces is simple. Plug in the USB and it works. Pair the Bluetooth ELM 327 OBD interface and it works. Linux and Raspberry Pi systems are a bit more challenging. 334 | 335 | On Linux/Raspberry Pi based systems, USB ELM 327 based OBD interfaces present as ```tty``` devices (e.g. ```/dev/ttyUSB0```). If software reports that the OBD interface can't be accessed, the problem may be one of permissions. Typically, ```tty``` devices are owned by ```root``` and group is set to ```dialout```. The user that is running the OBD data capture program must be a member of the same group (e.g. ```dialout```) as the ```tty``` device. 336 | 337 | ```bash 338 | # add dialout group to the current user's capabilities 339 | sudo adduser $(whoami) dialout 340 | ``` 341 | 342 | On Linux/Raspberry Pi, Bluetooth serial device creation is not automatic. After Bluetooth ELM 327 OBD interface has been paired, ```sudo rfcomm bind rfcomm0 ``` will create the required serial device. An example follows: 343 | 344 | ```bash 345 | # get the Bluetooth ELM 327 OBD interface's MAC (Media Access Control) address 346 | sudo bluetoothctl 347 | [bluetooth]# devices 348 | Device 00:00:00:33:33:33 OBDII 349 | [bluetooth]# exit 350 | # MAC Address for OBD is "00:00:00:33:33:33" 351 | 352 | # bind the Bluetooth ELM 327 OBD interface to a serial port/device using the interfaces Bluetooth MAC (Media Access Control) address: 353 | sudo rfcomm bind rfcomm0 00:00:00:33:33:33 354 | ``` 355 | 356 | Realize that before the above can work, the Bluetooth device must have already been paired to the Raspberry Pi. One easy way to pair is to use the Raspberry Pi's GUI to access the Bluetooth. The *pairing* and *trust* process is covered in [Pairing Bluetooth OBD Devices](./docs/README-BluetoothPairing.md). 357 | 358 | ![RaspberryPi Bluetooth GUI Utility](docs/README-rpi-gui-bt.jpg) 359 | 360 | On Linux/Raspberry Pi systems, the ```rfcomm``` command creates the device ```/dev/rfcomm0``` as a serial device owned by ```root``` and group ```dialout```. If multiple Bluetooth serial devices are paired and bound to ```/dev/rfcomm0```, ```/dev/rfcomm1```, ```/dev/rfcomm2``` and so on, OBD Logger will only automatically connect to the first device. The code can be modified to resolve this limitation. 361 | 362 | Regardless of connection type (USB or Bluetooth) to an ELM 327 OBD interface, the serial device will be owned by ```root``` with group ```dialout```. Access to the device is limited to ```root``` and users in the group ```dialout```. 363 | 364 | Users need to be added to the group ```dialout```. Assuming the user's username is ```human```: 365 | 366 | ```bash 367 | human@hostname:~ $ ls -l /dev/ttyUSB0 368 | crw-rw---- 1 root dialout 188, 0 Aug 13 15:47 /dev/ttyUSB0 369 | human@hostname:~ $ ls -l /dev/rfcomm0 370 | crw-rw---- 1 root dialout 120, 0 Aug 13 15:47 /dev/rfcomm0 371 | human@hostname:~ $ sudo adduser human dialout 372 | ``` 373 | 374 | ## Headless Operation On Raspberry Pi 375 | 376 | In order to reliably run in an automotive environment, the OBD Logger application needs to start automatically after all preconditions are satisfied. That is, the application must start without any user interaction. The trigger for starting the application is powering up the Raspberry Pi system. 377 | 378 | ### ```/etc/rc.local``` 379 | 380 | On the Raspberry Pi, commands embedded in "```/etc/rc.local```" will be run at the end of the system startup sequence by the ```root``` user. A sample "```/etc/rc.local```" follows: 381 | 382 | ```bash 383 | #!/bin/sh -e 384 | # 385 | # rc.local 386 | # 387 | # This script is executed at the end of each multiuser runlevel. 388 | # Make sure that the script will "exit 0" on success or any other 389 | # value on error. 390 | # 391 | # In order to enable or disable this script just change the execution 392 | # bits. 393 | # 394 | # By default this script does nothing. 395 | 396 | # Print the IP address 397 | _IP=$(hostname -I) || true 398 | if [ "$_IP" ]; then 399 | printf "My IP address is %s\n" "$_IP" 400 | fi 401 | 402 | # BEGIN TELEMETRY-OBD SUPPORT 403 | 404 | /bin/nohup "/root/bin/telemetry.rc.local.obd" & 405 | 406 | # END TELEMETRY-OBD SUPPORT 407 | 408 | exit 0 409 | ``` 410 | 411 | ```/etc/rc.local``` invokes ```/root/bin/telemetry.rc.local```. The functionality in ```/root/bin/telemetry.rc.local``` is not placed in ```/etc/rc.local``` for these reasons: 412 | 413 | * Rasberry Pi OS (```Bullseye```) invokes /etc/rc.local with ```/bin/sh``` (soft link to ```/bin/dash```) which is not the same as ```/usr/bin/bash```, the required shell. 414 | * ```/bin/sh``` is invoked with ```-e``` flag meaning that ```/etc/rc.local``` will stop execution when a pipe fails. See [bash documentation](https://www.gnu.org/software/bash/manual/bash.pdf). 415 | * The command ```bluetoothctl```, required to automatically detect and connect to the correct Bluetooth device, generates a pipe failure fault when run in ```/etc/rc.local```. It will run fine as ```root``` in the terminal. 416 | 417 | ### ```telemetry-obd/root/bin/telemetry.rc.local.obd``` 418 | 419 | ```telemetry-obd/root/bin/telemetry.rc.local.obd``` must be run as root. Once the Bluetooth subsystem is configured correctly, it invokes ```bin/obd_logger.sh``` which invokes ```obd_logger.py``` provided in this distribution. 420 | 421 | Shell variables, like ```OBD_USER``` must be changed in ```root/bin/telemetry.rc.local``` to match the target system. The line ```for BT_MAC_ADDR in "00:04:3E:5A:A7:67" "00:19:5D:26:4B:5F"``` must also be changed. the Bluetooth Media Access Control layer addresses (```"00:04:3E:5A:A7:67" and "00:19:5D:26:4B:5F"```) will need changing to match the target Bluetooth OBD dongle devices. This address, like an Internet address, must match your current configuration. Assuming your Bluetooth OBD adapter is currently paired to your Raspberry Pi, click on the Bluetooth icon on your Raspberry Pi desktop and select the ```Devices``` option. 422 | 423 | ![RaspberryPi Bluetooth GUI Utility Devices Dialog](docs/BT-connections.jpg) 424 | 425 | The yellow arrow points to where the Bluetooth MAC address is found on the ```Devices``` dialog box. 426 | 427 | The ```runuser``` command in "```telemetry-obd/root/bin/telemetry.rc.local.obd```" file runs the "```telemetry-obd/bin/obd_logger.sh```" ```bash``` shell program as user "```human```" and group "```dialout```". 428 | 429 | Once the ```telemetry-obd/root/bin/telemetry.rc.local.obd``` file has been modified, it must be copied to ```/root/bin``` and the file permissions changed: 430 | 431 | ```bash 432 | $ cd 433 | $ cd telemetry-obd/root/bin 434 | $ sudo mkdir /root/bin 435 | $ sudo cp telemetry.rc.local.obd /root/bin 436 | $ sudo chmod 0755 /root/bin/telemetry.rc.local.obd 437 | $ sudo ls -l /root/bin/telemetry.rc.local.obd 438 | $ sudo ls -l /root/bin/telemetry.rc.local.obd 439 | -rwxr-xr-x 1 root root 1503 May 31 10:16 /root/bin/telemetry.rc.local.obd 440 | $ 441 | ``` 442 | 443 | Make both ```obd_logger.sh``` and ```obd_tester.sh``` executable by using the command ```chmod +x obd_logger.sh obd_tester.sh```. 444 | 445 | ## Date/Time Accuracy During Data Collection 446 | 447 | After the power has been off, an unmodified Raspberry Pi will do one of the following to determine the time it starts up with: 448 | 449 | - starts up with the time value stored on disk 450 | - goes out to the Internet to a Network Time Protocol (NTP) server to get the current time 451 | 452 | While the Raspberry Pi runs, time updates as expected in its built-in clock and it periodically saves the current time value to disk. Because the built-in clock only works while the power is on, when the power goes off, the clock stops working. When power comes back on, the clock starts up from zero time. During the boot process, the clock gets updated from the disk and later, after the network starts up, an NTP server. No network, no time update. 453 | 454 | Each bit of data collected is collected with timestamps. Data and log file names have embedded timestamps in them. When the clock is hours or even weeks behind the actual time, data analysis becomes more difficult as it is hard to connect OBD data with driver activity such as stops for fuel or stops at destinations. 455 | 456 | One solution to consider is to always provide the Raspberry Pi with Internet access, especially during the boot process. For example, mobile phones (with the correct carrier plan) support mobile WIFI hotspot capability. In iPhone settings, this shows up in the **Settings** app as **Personal Hotspot**. 457 | 458 | On an iPhone, the **Personal Hotspot** times out and goes to sleep when no devices are connected to it. Before starting the vehicle, disable the hotspot and reenable it through the iPhone **Settings** app. This approach worked flawlessly on a two day, 1,000 mile trip with seven fuel stops, one overnight stop and several random health stops. 459 | 460 | Running Raspberry Pi's in headless mode requires WIFI to be configured in advance. For example, put each phone or tablet into mobile hotspot mode and then configure the Raspberry Pi to automatically connect to them before using the logging system in a vehicle. 461 | 462 | A possible solution is to use add a GPS receiver to the Raspberry Pi to add [Stratum-1 NTP Server](https://www.satsignal.eu/ntp/Raspberry-Pi-NTP.html) capability to the Raspberry Pi. This works in remote environments were mobile wireless signals are unavailable. It also requires less work on behalf of the vehicle operator. 463 | 464 | The solution currently in use a Raspberry Pi UPS HAT with a built-in real-time clock. This option, using the [Raspberry Pi UPS HAT](https://www.pishop.us/product/raspberry-pi-ups-hat/) had been working well with no operator intervention required. However, once the Lithium battery got funky, the built-in real-time clock often lost power. Power loss resets the clock to 1970 almost every time the system boots. 465 | 466 | In environments where the following are unavailable: 467 | 468 | - battery backed real-time clock 469 | - Internet access 470 | - GPS coupled with local Stratum-1 NTP Server 471 | 472 | The function ```get_output_file_name()``` from [Telemetry System Boot and Application Startup Counter](https://github.com/thatlarrypearson/telemetry-counter) has been added to ```obd_logger``` and ```obd_tester``` to ensure the creation of data files with unique invariant identifiers. These file names assure that data files can be processed in the order they were created. For the file naming to work properly, ```obd_logger``` and ```obd_tester``` need to be started through the bash startup programs found in ```telemetry-obd/bin/``` named ```obd_logger.sh``` and ```obd_tester.sh```. 473 | 474 | Data timestamp information may still need downstream processing using embedded GPS data to recalibrate system timestamp data. Examples for this type of downstream processing can be found in the ```obd_log_to_csv``` package. See [Telemetry OBD Data To CSV File](https://github.com/thatlarrypearson/telemetry-obd-log-to-csv). 475 | 476 | ## Running Raspberry Pi In Vehicle 477 | 478 | Getting the Raspberry Pi and OBD interface to work reliably in running vehicles turned out to be problematic. The initial setup used a USB OBD interface. The thinking was that a hard wired USB connection between the Raspberry Pi and the ODB interface would be simpler and more reliable. On the 2013 Jeep Wrangler Rubicon, this was true. The 110 VAC power adapter was plugged into the Jeep's 110 VAC outlet. 479 | 480 | However, both the 2017 Ford F-450 Truck and 2019 Ford EcoSport SUV wouldn't power the Raspberry Pi if it was connected via USB to an OBD interface. It didn't matter if the Pi was plugged into 12 VDC or 110 VAC outlets. It wasn't until a 600 Watt Sine Wave Inverter was used to power the Raspberry Pi that the underlying problem became clear. The inverter has [GFCI](https://www.bobvila.com/articles/gfci-outlets/) circuitry that tripped soon after the Raspberry Pi started communicating through USB to the OBD interface. There wasn't adequate electrical isolation between the vehicle OBD port and the Raspberry Pi. 481 | 482 | Given that electrical isolation was an issue, it became clear that wireless connection between components would be necessary. This is why Bluetooth became the preferred solution. 483 | 484 | Depending on the power supply powering the Raspberry Pi, there may also be issues with power when powering the Pi directly through the vehicle. Switching to a portable 12 VDC battery also made the solution more stable. 485 | 486 | ## Driver Responsibilities 487 | 488 | ![Run Cycles](docs/README-DriveSequence.JPG) 489 | 490 | Before turning on the ignition: 491 | 492 | - (Optional) Enable mobile hotspot 493 | - Plug OBD extension cord into vehicle OBD port. 494 | - Plug Bluetooth ELM 327 OBD interface into OBD extension cord. 495 | - Turn on vehicle ignition and start the vehicle. 496 | 497 | After vehicle is running: 498 | 499 | - Connect Bluetooth enabled Raspberry Pi to power. 500 | - Watch Bluetooth ELM 327 OBD interface lights to ensure that the Raspberry Pi is interacting with the interface within a minute or two. No flashing lights indicates failure. 501 | 502 | After turning off the vehicle: 503 | 504 | - Disconnect power from Raspberry Pi. 505 | 506 | ## Software Testing 507 | 508 | Software was tested manually using a [Freematics OBD-II Emulator](https://freematics.com/products/freematics-obd-emulator-mk2/) (vehicle emulator) as well as in actual vehicles. The test environment is as follows: 509 | 510 | ![Run Cycles](docs/README-TestEnvironment.JPG) 511 | 512 | The Freematics OBD-II Emulator does not cover all available OBD commands. This is especially true for the additional commands provided through ```add_commands.py```. Be aware that actual vehicle responses may not match the software. Also be aware that test code coverage in ```add_commands.py``` is sketchy at best. Your mileage may vary. 513 | 514 | ## Manufacturer Warranty Information 515 | 516 | The 2019 Ford EcoSport manual has the following statement with respect to aftermarket OBD devices: 517 | 518 | "_Your vehicle has an OBD Data Link 519 | Connector (DLC) that is used in 520 | conjunction with a diagnostic scan tool for 521 | vehicle diagnostics, repairs and 522 | reprogramming services. Installing an 523 | aftermarket device that uses the DLC 524 | during normal driving for purposes such as 525 | remote insurance company monitoring, 526 | transmission of vehicle data to other 527 | devices or entities, or altering the 528 | performance of the vehicle, may cause 529 | interference with or even damage to 530 | vehicle systems. We do not recommend 531 | or endorse the use of aftermarket plug-in 532 | devices unless approved by Ford. The 533 | vehicle Warranty will not cover damage 534 | caused by an aftermarket plug-in device._" 535 | 536 | You use this software at your own risk. 537 | 538 | ## LICENSE 539 | 540 | [MIT License](./LICENSE.md) 541 | -------------------------------------------------------------------------------- /bin/Install_telemetry-obd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | # Install_telemetry-obd.sh 3 | # 4 | 5 | export APP_HOME="/home/$(whoami)/telemetry-obd" 6 | export APP_PYTHON="/home/$(whoami)/.local/bin/python3.11" 7 | export DEBUG="True" 8 | 9 | # Debugging support 10 | if [ "${DEBUG}" = "True" ] 11 | then 12 | # enable shell debug mode 13 | set -x 14 | fi 15 | 16 | cd ${APP_HOME} 17 | 18 | if [ -d "${APP_HOME}/dist" ] 19 | then 20 | rm -rf "${APP_HOME}/dist" 21 | fi 22 | 23 | ${APP_PYTHON} -m pip uninstall -y telemetry-obd 24 | 25 | ${APP_PYTHON} -m build . 26 | ls -l dist/*.whl 27 | ${APP_PYTHON} -m pip install dist/*.whl 28 | 29 | ${APP_PYTHON} -m telemetry_obd.obd_logger --help 30 | ${APP_PYTHON} -m telemetry_obd.obd_command_tester --help 31 | -------------------------------------------------------------------------------- /bin/obd_logger.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | # obd_logger.sh 3 | # 4 | # Runs OBD Logger 5 | 6 | # Need time for the system to startup the Bluetooth connection 7 | export STARTUP_DELAY=10 8 | 9 | # Need time for system/vehicle OBD interface recover after failure 10 | export RESTART_DELAY=60 11 | 12 | export APP_ID="obd" 13 | export APP_HOME="/home/$(whoami)/telemetry-data" 14 | export APP_CONFIG_DIR="${APP_HOME}/config" 15 | export APP_TMP_DIR="${APP_HOME}/tmp" 16 | export APP_BASE_PATH="${APP_HOME}/data" 17 | export APP_FULL_CYCLES=10000 18 | export APP_TEST_CYCLES=100 19 | export APP_PYTHON="/home/$(whoami)/.local/bin/python3.11" 20 | export DEBUG="True" 21 | export TIMEOUT=4.0 22 | 23 | # get next application startup counter 24 | export APP_COUNT=$(${APP_PYTHON} -m tcounter.app_counter ${APP_ID}) 25 | 26 | # get current system startup counter 27 | export BOOT_COUNT=$(${APP_PYTHON} -m tcounter.boot_counter --current_boot_count) 28 | 29 | export APP_LOG_FILE="telemetry-${BOOT_COUNT}-${APP_ID}-${APP_COUNT}.log" 30 | 31 | # Run Command Tester one time if following file exists 32 | export COMMAND_TESTER="${APP_HOME}/RunCommandTester" 33 | export COMMAND_TESTER_DELAY=60 34 | 35 | # Debugging support 36 | if [ "${DEBUG}" = "True" ] 37 | then 38 | # enable shell debug mode 39 | set -x 40 | fi 41 | 42 | if [ ! -d "${APP_TMP_DIR}" ] 43 | then 44 | mkdir --parents "${APP_TMP_DIR}" 45 | fi 46 | 47 | # turn off stdin 48 | 0<&- 49 | 50 | # redirect all stdout and stderr to file 51 | exec &> "${APP_TMP_DIR}/${APP_LOG_FILE}" 52 | 53 | date '+%Y/%m/%d %H:%M:%S' 54 | 55 | if [ ! -d "${APP_BASE_PATH}" ] 56 | then 57 | mkdir --parents "${APP_BASE_PATH}" 58 | fi 59 | 60 | if [ ! -d "${APP_CONFIG_DIR}" ] 61 | then 62 | mkdir --parents "${APP_CONFIG_DIR}" 63 | fi 64 | 65 | cd "${APP_HOME}" 66 | 67 | sleep ${STARTUP_DELAY} 68 | 69 | if [ -f "${COMMAND_TESTER}" ] 70 | then 71 | # get next application startup counter 72 | export TEST_APP_COUNT=$(${APP_PYTHON} -m tcounter.app_counter 'obd-cmd-test') 73 | echo ${TEST_APP_COUNT} 74 | 75 | ${APP_PYTHON} -m telemetry_obd.obd_command_tester \ 76 | --timeout "${TIMEOUT}" \ 77 | --no_fast \ 78 | --cycle "${APP_TEST_CYCLES}" \ 79 | "${APP_BASE_PATH}" 80 | 81 | export RtnVal="$?" 82 | echo obd_command_tester returns "${RtnVal}" 83 | 84 | rm -f "${COMMAND_TESTER}" 85 | sleep "${COMMAND_TESTER_DELAY}" 86 | fi 87 | 88 | while date '+%Y/%m/%d %H:%M:%S' 89 | do 90 | ${APP_PYTHON} -m telemetry_obd.obd_logger \ 91 | --timeout "${TIMEOUT}" \ 92 | --no_fast \ 93 | --config_dir "${APP_CONFIG_DIR}" \ 94 | --full_cycles "${APP_FULL_CYCLES}" \ 95 | "${APP_BASE_PATH}" 96 | 97 | export RtnVal="$?" 98 | echo obd_logger returns "${RtnVal}" 99 | 100 | sleep "${RESTART_DELAY}" 101 | done 102 | 103 | -------------------------------------------------------------------------------- /bin/obd_tester.sh: -------------------------------------------------------------------------------- 1 | # obd_logger.sh 2 | # 3 | # Runs OBD Tester to test all known possible OBD commands 4 | 5 | # Need time for the system to startup the Bluetooth connection 6 | export STARTUP_DELAY=10 7 | 8 | export APP_ID="obd" 9 | export APP_HOME="/home/$(whoami)/telemetry-data" 10 | export APP_TMP_DIR="${APP_HOME}/tmp" 11 | export APP_BASE_PATH="${APP_HOME}/data" 12 | export APP_TEST_CYCLES=5 13 | export APP_PYTHON="/home/$(whoami)/.local/bin/python3.11" 14 | 15 | # get next application startup counter 16 | export APP_COUNT=$(${APP_PYTHON} -m tcounter.app_counter ${APP_ID}) 17 | 18 | # get current system startup counter 19 | export BOOT_COUNT=$(${APP_PYTHON} -m tcounter.boot_counter --current_boot_count) 20 | 21 | export APP_LOG_FILE="telemetry-${BOOT_COUNT}-${APP_ID}-${APP_COUNT}.log" 22 | 23 | # uncomment to turn off stdin 24 | # 0<&- 25 | 26 | # uncomment to redirect all stdout and stderr to file 27 | # exec &> "${APP_TMP_DIR}/${APP_LOG_FILE}" 28 | 29 | if [ ! -d "${APP_BASE_PATH}" ] 30 | then 31 | mkdir --parents "${APP_BASE_PATH}" 32 | fi 33 | 34 | if [ ! -d "${APP_TMP_DIR}" ] 35 | then 36 | mkdir --parents "${APP_TMP_DIR}" 37 | fi 38 | 39 | cd "${APP_HOME}" 40 | 41 | sleep ${STARTUP_DELAY} 42 | 43 | ${APP_PYTHON} -m telemetry_obd.obd_command_tester \ 44 | --cycle "${APP_TEST_CYCLES}" \ 45 | --verbose --logging \ 46 | "${APP_BASE_PATH}" 47 | 48 | export RtnVal="$?" 49 | echo 50 | echo obd_command_tester returns "${RtnVal}" 51 | date '+%Y/%m/%d %H:%M:%S' 52 | -------------------------------------------------------------------------------- /config/default.ini: -------------------------------------------------------------------------------- 1 | [STARTUP NAMES] 2 | startup = 3 | ABSOLUTE_LOAD 4 | ACCELERATOR_POS_D 5 | ACCELERATOR_POS_E 6 | ACCELERATOR_POS_F 7 | AIR_STATUS 8 | AMBIANT_AIR_TEMP 9 | AUX_INPUT_STATUS 10 | BAROMETRIC_PRESSURE 11 | CALIBRATION_ID 12 | CALIBRATION_ID_MESSAGE_COUNT 13 | CATALYST_TEMP_B1S1 14 | CATALYST_TEMP_B1S2 15 | CATALYST_TEMP_B2S1 16 | CATALYST_TEMP_B2S2 17 | COMMANDED_EGR 18 | COMMANDED_EQUIV_RATIO 19 | CONTROL_MODULE_VOLTAGE 20 | COOLANT_TEMP 21 | CVN 22 | CVN_MESSAGE_COUNT 23 | DISTANCE_SINCE_DTC_CLEAR 24 | DISTANCE_W_MIL 25 | ECU_NAME 26 | ECU_NAME_MESSAGE_COUNT 27 | EGR_ERROR 28 | EMISSION_REQ 29 | ENGINE_LOAD 30 | ETHANOL_PERCENT 31 | EVAPORATIVE_PURGE 32 | EVAP_VAPOR_PRESSURE 33 | EVAP_VAPOR_PRESSURE_ABS 34 | EVAP_VAPOR_PRESSURE_ALT 35 | FREEZE_DTC 36 | FUEL_INJECT_TIMING 37 | FUEL_LEVEL 38 | FUEL_PRESSURE 39 | FUEL_RAIL_PRESSURE_ABS 40 | FUEL_RAIL_PRESSURE_DIRECT 41 | FUEL_RAIL_PRESSURE_VAC 42 | FUEL_RATE 43 | FUEL_STATUS 44 | FUEL_TYPE 45 | HYBRID_BATTERY_REMAINING 46 | INTAKE_PRESSURE 47 | INTAKE_TEMP 48 | LONG_FUEL_TRIM_1 49 | LONG_FUEL_TRIM_2 50 | LONG_O2_TRIM_B1 51 | LONG_O2_TRIM_B2 52 | MAF 53 | MASS_AIR_FLOW_SENSOR-sensor_a 54 | MAX_MAF 55 | MAX_VALUES 56 | O2_B1S1 57 | O2_B1S2 58 | O2_B1S3 59 | O2_B1S4 60 | O2_B2S1 61 | O2_B2S2 62 | O2_B2S3 63 | O2_B2S4 64 | O2_S1_WR_CURRENT 65 | O2_S1_WR_VOLTAGE 66 | O2_S2_WR_CURRENT 67 | O2_S2_WR_VOLTAGE 68 | O2_S3_WR_CURRENT 69 | O2_S3_WR_VOLTAGE 70 | O2_S4_WR_CURRENT 71 | O2_S4_WR_VOLTAGE 72 | O2_S5_WR_CURRENT 73 | O2_S5_WR_VOLTAGE 74 | O2_S6_WR_CURRENT 75 | O2_S6_WR_VOLTAGE 76 | O2_S7_WR_CURRENT 77 | O2_S7_WR_VOLTAGE 78 | O2_S8_WR_CURRENT 79 | O2_S8_WR_VOLTAGE 80 | O2_SENSORS 81 | O2_SENSORS_ALT 82 | OBD_COMPLIANCE 83 | OIL_TEMP 84 | PERCENT_TORQUE-Idle 85 | PERF_TRACKING_COMPRESSION 86 | PERF_TRACKING_MESSAGE_COUNT 87 | PERF_TRACKING_SPARK 88 | PIDS_9A 89 | PIDS_A 90 | PIDS_B 91 | PIDS_C 92 | PIDS_D 93 | REFERENCE_TORQUE 94 | RELATIVE_ACCEL_POS 95 | RELATIVE_THROTTLE_POS 96 | RPM 97 | RUN_TIME 98 | RUN_TIME_MIL 99 | SHORT_FUEL_TRIM_1 100 | SHORT_FUEL_TRIM_2 101 | SHORT_O2_TRIM_B1 102 | SHORT_O2_TRIM_B2 103 | SPEED 104 | STATUS 105 | STATUS_DRIVE_CYCLE 106 | THROTTLE_ACTUATOR 107 | THROTTLE_POS 108 | THROTTLE_POS_B 109 | THROTTLE_POS_C 110 | TIME_SINCE_DTC_CLEARED 111 | TIMING_ADVANCE 112 | TORQUE 113 | TORQUE_DEMAND 114 | VIN 115 | VIN_MESSAGE_COUNT 116 | WARMUPS_SINCE_DTC_CLEAR 117 | 118 | 119 | [HOUSEKEEPING NAMES] 120 | housekeeping = 121 | ABSOLUTE_LOAD 122 | ACCELERATOR_POS_D 123 | ACCELERATOR_POS_E 124 | ACCELERATOR_POS_F 125 | AIR_STATUS 126 | AMBIANT_AIR_TEMP 127 | AUX_INPUT_STATUS 128 | BAROMETRIC_PRESSURE 129 | CALIBRATION_ID 130 | CALIBRATION_ID_MESSAGE_COUNT 131 | CATALYST_TEMP_B1S1 132 | CATALYST_TEMP_B1S2 133 | CATALYST_TEMP_B2S1 134 | CATALYST_TEMP_B2S2 135 | COMMANDED_EGR 136 | COMMANDED_EQUIV_RATIO 137 | CONTROL_MODULE_VOLTAGE 138 | COOLANT_TEMP 139 | CVN 140 | CVN_MESSAGE_COUNT 141 | DISTANCE_SINCE_DTC_CLEAR 142 | DISTANCE_W_MIL 143 | ECU_NAME 144 | ECU_NAME_MESSAGE_COUNT 145 | EGR_ERROR 146 | EMISSION_REQ 147 | ENGINE_LOAD 148 | ETHANOL_PERCENT 149 | EVAPORATIVE_PURGE 150 | EVAP_VAPOR_PRESSURE 151 | EVAP_VAPOR_PRESSURE_ABS 152 | EVAP_VAPOR_PRESSURE_ALT 153 | FREEZE_DTC 154 | FUEL_INJECT_TIMING 155 | FUEL_LEVEL 156 | FUEL_PRESSURE 157 | FUEL_RAIL_PRESSURE_ABS 158 | FUEL_RAIL_PRESSURE_DIRECT 159 | FUEL_RAIL_PRESSURE_VAC 160 | FUEL_RATE 161 | FUEL_STATUS 162 | FUEL_TYPE 163 | HYBRID_BATTERY_REMAINING 164 | INTAKE_PRESSURE 165 | INTAKE_TEMP 166 | LONG_FUEL_TRIM_1 167 | LONG_FUEL_TRIM_2 168 | LONG_O2_TRIM_B1 169 | LONG_O2_TRIM_B2 170 | MAF 171 | MASS_AIR_FLOW_SENSOR-sensor_a 172 | MAX_MAF 173 | MAX_VALUES 174 | O2_B1S1 175 | O2_B1S2 176 | O2_B1S3 177 | O2_B1S4 178 | O2_B2S1 179 | O2_B2S2 180 | O2_B2S3 181 | O2_B2S4 182 | O2_S1_WR_CURRENT 183 | O2_S1_WR_VOLTAGE 184 | O2_S2_WR_CURRENT 185 | O2_S2_WR_VOLTAGE 186 | O2_S3_WR_CURRENT 187 | O2_S3_WR_VOLTAGE 188 | O2_S4_WR_CURRENT 189 | O2_S4_WR_VOLTAGE 190 | O2_S5_WR_CURRENT 191 | O2_S5_WR_VOLTAGE 192 | O2_S6_WR_CURRENT 193 | O2_S6_WR_VOLTAGE 194 | O2_S7_WR_CURRENT 195 | O2_S7_WR_VOLTAGE 196 | O2_S8_WR_CURRENT 197 | O2_S8_WR_VOLTAGE 198 | O2_SENSORS 199 | O2_SENSORS_ALT 200 | OBD_COMPLIANCE 201 | OIL_TEMP 202 | PERCENT_TORQUE-Idle 203 | PERF_TRACKING_COMPRESSION 204 | PERF_TRACKING_MESSAGE_COUNT 205 | PERF_TRACKING_SPARK 206 | PIDS_9A 207 | PIDS_A 208 | PIDS_B 209 | PIDS_C 210 | PIDS_D 211 | REFERENCE_TORQUE 212 | RELATIVE_ACCEL_POS 213 | RELATIVE_THROTTLE_POS 214 | RPM 215 | RUN_TIME 216 | RUN_TIME_MIL 217 | SHORT_FUEL_TRIM_1 218 | SHORT_FUEL_TRIM_2 219 | SHORT_O2_TRIM_B1 220 | SHORT_O2_TRIM_B2 221 | SPEED 222 | STATUS 223 | STATUS_DRIVE_CYCLE 224 | THROTTLE_ACTUATOR 225 | THROTTLE_POS 226 | THROTTLE_POS_B 227 | THROTTLE_POS_C 228 | TIME_SINCE_DTC_CLEARED 229 | TIMING_ADVANCE 230 | TORQUE 231 | TORQUE_DEMAND 232 | VIN 233 | VIN_MESSAGE_COUNT 234 | WARMUPS_SINCE_DTC_CLEAR 235 | 236 | 237 | [CYCLE NAMES] 238 | cycle = 239 | ABSOLUTE_LOAD 240 | ACCELERATOR_POS_D 241 | ACCELERATOR_POS_E 242 | ACCELERATOR_POS_F 243 | AIR_STATUS 244 | AMBIANT_AIR_TEMP 245 | AUX_INPUT_STATUS 246 | BAROMETRIC_PRESSURE 247 | CALIBRATION_ID 248 | CALIBRATION_ID_MESSAGE_COUNT 249 | CATALYST_TEMP_B1S1 250 | CATALYST_TEMP_B1S2 251 | CATALYST_TEMP_B2S1 252 | CATALYST_TEMP_B2S2 253 | COMMANDED_EGR 254 | COMMANDED_EQUIV_RATIO 255 | CONTROL_MODULE_VOLTAGE 256 | COOLANT_TEMP 257 | CVN 258 | CVN_MESSAGE_COUNT 259 | DISTANCE_SINCE_DTC_CLEAR 260 | DISTANCE_W_MIL 261 | ECU_NAME 262 | ECU_NAME_MESSAGE_COUNT 263 | EGR_ERROR 264 | EMISSION_REQ 265 | ENGINE_LOAD 266 | ETHANOL_PERCENT 267 | EVAPORATIVE_PURGE 268 | EVAP_VAPOR_PRESSURE 269 | EVAP_VAPOR_PRESSURE_ABS 270 | EVAP_VAPOR_PRESSURE_ALT 271 | FREEZE_DTC 272 | FUEL_INJECT_TIMING 273 | FUEL_LEVEL 274 | FUEL_PRESSURE 275 | FUEL_RAIL_PRESSURE_ABS 276 | FUEL_RAIL_PRESSURE_DIRECT 277 | FUEL_RAIL_PRESSURE_VAC 278 | FUEL_RATE 279 | FUEL_STATUS 280 | FUEL_TYPE 281 | HYBRID_BATTERY_REMAINING 282 | INTAKE_PRESSURE 283 | INTAKE_TEMP 284 | LONG_FUEL_TRIM_1 285 | LONG_FUEL_TRIM_2 286 | LONG_O2_TRIM_B1 287 | LONG_O2_TRIM_B2 288 | MAF 289 | MASS_AIR_FLOW_SENSOR-sensor_a 290 | MAX_MAF 291 | MAX_VALUES 292 | O2_B1S1 293 | O2_B1S2 294 | O2_B1S3 295 | O2_B1S4 296 | O2_B2S1 297 | O2_B2S2 298 | O2_B2S3 299 | O2_B2S4 300 | O2_S1_WR_CURRENT 301 | O2_S1_WR_VOLTAGE 302 | O2_S2_WR_CURRENT 303 | O2_S2_WR_VOLTAGE 304 | O2_S3_WR_CURRENT 305 | O2_S3_WR_VOLTAGE 306 | O2_S4_WR_CURRENT 307 | O2_S4_WR_VOLTAGE 308 | O2_S5_WR_CURRENT 309 | O2_S5_WR_VOLTAGE 310 | O2_S6_WR_CURRENT 311 | O2_S6_WR_VOLTAGE 312 | O2_S7_WR_CURRENT 313 | O2_S7_WR_VOLTAGE 314 | O2_S8_WR_CURRENT 315 | O2_S8_WR_VOLTAGE 316 | O2_SENSORS 317 | O2_SENSORS_ALT 318 | OBD_COMPLIANCE 319 | OIL_TEMP 320 | PERCENT_TORQUE-Idle 321 | PERF_TRACKING_COMPRESSION 322 | PERF_TRACKING_MESSAGE_COUNT 323 | PERF_TRACKING_SPARK 324 | PIDS_9A 325 | PIDS_A 326 | PIDS_B 327 | PIDS_C 328 | PIDS_D 329 | REFERENCE_TORQUE 330 | RELATIVE_ACCEL_POS 331 | RELATIVE_THROTTLE_POS 332 | RPM 333 | RUN_TIME 334 | RUN_TIME_MIL 335 | SHORT_FUEL_TRIM_1 336 | SHORT_FUEL_TRIM_2 337 | SHORT_O2_TRIM_B1 338 | SHORT_O2_TRIM_B2 339 | SPEED 340 | STATUS 341 | STATUS_DRIVE_CYCLE 342 | THROTTLE_ACTUATOR 343 | THROTTLE_POS 344 | THROTTLE_POS_B 345 | THROTTLE_POS_C 346 | TIME_SINCE_DTC_CLEARED 347 | TIMING_ADVANCE 348 | TORQUE 349 | TORQUE_DEMAND 350 | VIN 351 | VIN_MESSAGE_COUNT 352 | WARMUPS_SINCE_DTC_CLEAR 353 | 354 | -------------------------------------------------------------------------------- /docs/BT-connections.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/BT-connections.jpg -------------------------------------------------------------------------------- /docs/Python310-Install.md: -------------------------------------------------------------------------------- 1 | # Python 3.10 Installation Instructions 2 | 3 | These instructions are for installing Python 3.10 on a Raspberry Pi 4 computer running Rasberry Pi OS Bullseye version 11.3. With some (or no) modification, these instructions will work on any recent Debian release based Linux distributions. 4 | 5 | Ensure that the Raspberry Pi software is completely updated to the most recent release. One way to do this is as follows: 6 | 7 | ```bash 8 | # update and upgrade Linux/Raspberry Pi OS 9 | sudo apt update 10 | sudo apt upgrade -y 11 | sudo apt autoremove -y 12 | sudo shutdown -r now 13 | sudo apt dist-upgrade -y 14 | sudo shutdown -r now 15 | ``` 16 | 17 | Run the test to determine if Python 3.10 is already installed. 18 | 19 | ```bash 20 | # Test to determine if Python 3.10 is already installed 21 | python3.10 --version 22 | ``` 23 | 24 | A ```commmand not found``` response means *python 3.10* must be installed. 25 | 26 | If ```Python 3.10```, isn't already installed you will need to make it from source to install it. 27 | 28 | Go to the [Python Downloads](https://www.python.org/downloads/source/) page. Find the most recent version of Python 3.10 from the list. Currently, the latest 3.10 release is at version 3.10.4. The build instructions below assume python3.10.4. 29 | 30 | The following commands install all of the system libraries required to build Python 3.10 from source code. 31 | 32 | ```bash 33 | # install build tools 34 | sudo apt-get update 35 | sudo apt-get upgrade -y 36 | sudo apt-get install -y build-essential checkinstall 37 | 38 | # look for Raspberry Pi OS version, e.g. '11.3' 39 | cat /etc/debian_version 40 | 41 | # look for VERSION_CODENAME, e.g. 'bullseye' 42 | cat /etc/os-release | grep VERSION_CODENAME 43 | 44 | # Raspberry Pi OS versions before 11.3 Bullseye 45 | # sudo apt-get install -y libreadline-gplv2-dev 46 | 47 | # Raspberry Pi OS version 11.3 Bullseye 48 | sudo apt-get install -y libreadline-dev 49 | 50 | sudo apt-get install -y libncursesw5-dev libssl-dev 51 | sudo apt-get install -y libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev zlib1g-dev 52 | ``` 53 | 54 | The following builds Python 3.10 from source code. 55 | 56 | ```bash 57 | # the following makes and installs python3.10 into /usr/local/bin 58 | # with the libraries in /usr/local/lib. 59 | cd 60 | wget https://www.python.org/ftp/python/3.10.13/Python-3.10.13.tgz 61 | cd /opt 62 | sudo tar xvzf ~/Python-3.10.13.tgz 63 | cd Python-3.10.13 64 | 65 | # compile Python 3.10 66 | sudo ./configure --enable-optimizations 67 | 68 | # install compiled Python 3.10 69 | sudo make altinstall 70 | 71 | # cleanup 72 | sudo make clean 73 | cd /opt 74 | sudo rm -rf Python-3.10.13 75 | 76 | # test installation 77 | python3.10 --version 78 | ``` 79 | 80 | All is well when ```Python 3.10.13``` is returned by the ```python3.10 --version``` command. 81 | 82 | The latest available production version of Python 3.10 should be used when available. The latest versions of source code can always be found on the [Python Source Releases](https://www.python.org/downloads/source/) web page. Just scan down the list for the first Python 3.10 version. 83 | 84 | ## LICENSE 85 | 86 | [MIT License](../LICENSE.md) 87 | -------------------------------------------------------------------------------- /docs/Python311-Install.md: -------------------------------------------------------------------------------- 1 | # Python 3.11 Installation Instructions 2 | 3 | These instructions are for installing Python 3.11 on a Raspberry Pi 4 computer running Rasberry Pi OS Bookworm version 12.2. With some (or no) modification, these instructions will work on any recent Debian release based Linux distributions. 4 | 5 | The latest available production version of Python 3.11 should be used when available. The latest versions of source code can always be found on the [Python Source Releases](https://www.python.org/downloads/source/) web page. Just scan down the list for the first (and latest) Python 3.11 version. 6 | 7 | Ensure that the Raspberry Pi software is completely updated to the most recent release. One way to do this is as follows: 8 | 9 | ```bash 10 | # update and upgrade Linux/Raspberry Pi OS 11 | sudo apt update 12 | sudo apt upgrade -y 13 | sudo apt autoremove -y 14 | sudo shutdown -r now 15 | sudo apt dist-upgrade -y 16 | sudo shutdown -r now 17 | ``` 18 | 19 | Run the test to determine if Python 3.11 is already installed. 20 | 21 | ```bash 22 | # Test to determine if Python 3.11 is already installed 23 | python3.11 --version 24 | ``` 25 | 26 | A ```commmand not found``` response means *python 3.11* must be installed. 27 | 28 | If ```Python 3.11```, isn't already installed you will need to make it from source to install it. 29 | 30 | If ```python3.11``` responds with a version number, then you need to do the following test to see if it can work for you: 31 | 32 | ```bash 33 | python3.11 -m pip install pip --upgrade 34 | ``` 35 | 36 | If the first line of the response is "```error: externally-managed-environment```" then you can't use the system ```python3.11```. You must install your own private ```python3.11```. 37 | 38 | Go to the [Python Downloads](https://www.python.org/downloads/source/) page. Find the most recent version of Python 3.11 from the list. Currently, the latest 3.11 release is at version 3.11.6. The build instructions below assume python3.11.6. 39 | 40 | The following commands install all of the system libraries required to build Python 3.11 from source code. You will get better results if you upgrade your Raspberry Pi operating system to version ```12.2 bookworm``` or higher. See code below to get your operating system version information. 41 | 42 | ```bash 43 | # install build tools 44 | sudo apt-get update 45 | sudo apt-get upgrade -y 46 | sudo apt-get install -y build-essential checkinstall 47 | 48 | # look for Raspberry Pi OS version 49 | # e.g. '11.7' is `bullseye` 50 | # e.g. '12.2' is 'bookworm' 51 | cat /etc/debian_version 52 | 53 | # look for VERSION_CODENAME, e.g. 'bookworm' 54 | cat /etc/os-release | grep VERSION_CODENAME 55 | 56 | # Raspberry Pi OS versions before 11.0 Bullseye 57 | # sudo apt-get install -y libreadline-gplv2-dev 58 | # sudo apt-get install -y libncursesw5-dev libssl-dev 59 | # sudo apt-get install -y libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev zlib1g-dev 60 | 61 | # Raspberry Pi OS version 11.x Bullseye 62 | # sudo apt-get install -y libreadline-dev 63 | # sudo apt-get install -y libncursesw5-dev libssl-dev 64 | # sudo apt-get install -y libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev zlib1g-dev 65 | 66 | # Raspberry Pi OS version 12.x Bookworm 67 | sudo apt-get install -y libreadline-dev libgdbm-compat-dev liblzma-dev 68 | sudo apt-get install -y libncurses5-dev libnss3-dev libffi-dev 69 | sudo apt-get install -y libncursesw5-dev libssl-dev 70 | sudo apt-get install -y libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev zlib1g-dev 71 | ``` 72 | 73 | The following builds Python 3.11 from source code. 74 | 75 | ```bash 76 | # the following makes and installs python3.11 into /usr/local/bin 77 | # with the libraries in /usr/local/lib. 78 | cd 79 | wget https://www.python.org/ftp/python/3.11.6/Python-3.11.6.tgz 80 | tar xvzf ~/Python-3.11.6.tgz 81 | cd Python-3.11.6 82 | 83 | # configure build Python 3.11 84 | ./configure --enable-optimizations --prefix=${HOME}/.local --exec-prefix=${HOME}/.local --with-ensurepip=install 85 | 86 | # build/install compiled Python 3.11 87 | # the build process can be speeded up using "make --jobs=$(nproc) altinstall" 88 | # but this often fails for unknown reasons on Raspberry Pi 4B 4GB RAM 89 | make altinstall 90 | 91 | # cleanup 92 | make clean 93 | rm -rf Python-3.11.6 94 | 95 | # test installation 96 | ${HOME}/.local/bin/python3.11 --version 97 | ``` 98 | 99 | When ```Python 3.11.6``` is returned by the ```python3.11 --version``` command, then the python installation is complete. 100 | 101 | ## **Another Important Thing** 102 | 103 | The Python version just installed may not be in your execution path and it needs to be so that you can execute the correct ```python3.11``` from the command line. 104 | 105 | ```bash 106 | which python3.11 107 | ``` 108 | 109 | If this returns "```/home//.local/bin/python3.11```", then you may be good to go. 110 | 111 | First, check to see if "```.profile```" exists in your home directory and includes "'''.local/bin'''" in your path. 112 | 113 | ```bash 114 | cd 115 | cat .profile 116 | ``` 117 | 118 | You are good if you see "```PATH="$HOME/.local/bin:$PATH"```" in the following code ```bash``` shell code fragment at the end of the file: 119 | 120 | ```bash 121 | # set PATH so it includes user's private bin if it exists 122 | if [ -d "$HOME/.local/bin" ] ; then 123 | PATH="$HOME/.local/bin:$PATH" 124 | fi 125 | ``` 126 | 127 | If you don't see the above code fragment in your "```.profile```" file, then add it on at the end. 128 | 129 | ```bash 130 | # if you modified .profile 131 | source .profile 132 | which python3.11 133 | ``` 134 | 135 | This should return "```/home//.local/bin/python3.11```". If so, **Python 3.11 is correctly installed.** Otherwise start over. 136 | 137 | ## **Some More Important Things** 138 | 139 | Update ```pip```, the Python package installer. 140 | 141 | ```bash 142 | # Python pip Install Support 143 | python3.11 -m pip install --upgrade pip 144 | ``` 145 | 146 | Install package building tools. 147 | 148 | ```bash 149 | python3.11 -m pip install --upgrade wheel setuptools markdown build cython psutil 150 | ``` 151 | 152 | ## LICENSE 153 | 154 | [MIT License](../LICENSE.md) 155 | -------------------------------------------------------------------------------- /docs/README-BluetoothPairing.md: -------------------------------------------------------------------------------- 1 | # Pairing Bluetooth OBD Devices 2 | 3 | When purchasing Bluetooth OBD devices, ensure that each device is supported by the operating system. 4 | 5 | At some point, the Raspberry Pi 4 will need to pair with an OBD Interface that is compatible with the ```python-obd``` software library. Look for Bluetooth OBD interface hardware based on [ELM 327](https://www.elmelectronics.com/products/ics/obd/) chips at version 1.5 or greater. At the time you read this, ELM may be out of business. 6 | 7 | Newer Bluetooth OBD interfaces use different chip sets which work so long as they support the ELM 327 command language. E.g. [OBDLink MX+](https://www.obdlink.com/products/obdlink-mxp/) uses the [STN2100 MULTI-PROTOCOL OBD INTERPRETER IC](https://www.obdsol.com/solutions/chips/stn2100/) chip set family to good effect. 8 | 9 | When an OBD interface emulator is available, pairing the OBD device to the Raspberry Pi worked fine using the pairing program accessible through the Pi's GUI. The only complaint is there isn't much time to pair. In general, from the time the OBD interface is plugged into either the car or the emulator, there is less than 20 seconds to complete the pairing before the OBD interface turns off pairing. It can take a few attempts to pair. 10 | 11 | Pairing with the OBD device plugged into a vehicle is considerably more challenging. OBD interface extension cords are available. Extension cords are useful because the lights on the OBD interface can be seen. These lights are important while trying to pair. Lights also blink when the Pi is communicating to the OBD interface. 12 | 13 | One important tip for Bluetooth pairing: the process works better when the Bluetooth device is less than 30 feet from the computer. Pairing also works better with clear line of sight between device and computer. 14 | 15 | Starting over with the pairing process is easy. Just unplug the OBD device from the car and plug it back in five seconds later. 16 | 17 | After the Bluetooth software has been installed and the system rebooted, look for the Bluetooth icon in the upper right hand area of the display (green arrow). 18 | 19 | ![Bluetooth Icon](./README-rpi-gui-bt.jpg) 20 | 21 | Plug your Bluetooth OBD dongle into an OBD interface and if there is a pairing button to press, press it. 22 | 23 | ![Setup New Device](./README-rpi-gui-bt-devices.jpg) 24 | 25 | Click on the Bluetooth icon and select *Devices*. A dialog box containing paired devices will be displayed. 26 | 27 | ![Device](./README-rpi-gui-bt-devices-dialog.jpg) 28 | 29 | In the dialog box, select *Search*. This will show both paired and unpaired Bluetooth devices. Look for *OBD* devices. 30 | 31 | ![Device Search](./README-rpi-gui-bt-devices-search.jpg) 32 | 33 | *Search* shows paired and unpaired devices. Paired devices have *keys* and *stars* on their right. Find the device to pair and right-click. 34 | 35 | ![Device Search](./README-rpi-gui-bt-devices-search-pair.jpg) 36 | 37 | Then select *Pair* from the dropdown. 38 | 39 | ![Pairing Request](./README-rpi-gui-bt-devices-search-pairing-request.jpg) 40 | 41 | The *Pairing Request* dialog will require a PIN code to authenticate the Raspberry Pi to the Bluetooth device. This particular OBD dongle cost $15 on Amazon years ago and has the simple PIN code of *1234*. Generally, the PIN codes come in the same package as the OBD adapter. 42 | 43 | ![Bluetooth Authentication](./README-rpi-gui-bt-devices-search-pairing-authentication.jpg) 44 | 45 | Successful pairings provide a *Bluetooth Authentication* notification dialog. Note the MAC (Media Access Control) address to the right, formatted as six pairs of hex digits (*0123456789ABCDEF*). Write this address down. It will be needed in future steps. 46 | 47 | ![Bluetooth Authentication](./README-rpi-gui-bt-devices-authenticated.jpg) 48 | 49 | The *key* has shown up on the newly paired/authenticated device. However, there is no *star*. 50 | 51 | ![Trust](./README-rpi-gui-bt-devices-trust.jpg) 52 | 53 | To trust a device, earning a *star*, right-click on the device and select *Trust*. 54 | 55 | ![Trusted](./README-rpi-gui-bt-devices-trusted.jpg) 56 | 57 | Since the device is now *paired* and *trusted*, it is almost ready for use. 58 | 59 | ![Setup](./README-rpi-gui-bt-devices-setup.jpg) 60 | 61 | To use this device right now, a serial device must be associated with it. Right-click on the device and select *Setup* from the dropdown menu. 62 | 63 | ![Connect Dialog](./README-rpi-gui-bt-devices-connect.jpg) 64 | 65 | In the *Connect* dialog, select *Serial Port* as shown and click *Next*. 66 | 67 | ![Connect Failed](./README-rpi-gui-bt-devices-connect-failed.jpg) 68 | 69 | In the above, *Device added successfully, but failed to connect*. It didn't really fail. The OBD dongle is now accessible. 70 | 71 | Don't expect the Raspberry Pi to automatically connect to the OBD device after rebooting. How to automatically reconnect to Bluetooth devices is covered later in this documentation. 72 | 73 | ## LICENSE 74 | 75 | [MIT License](../LICENSE.md) 76 | -------------------------------------------------------------------------------- /docs/README-DriveSequence.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-DriveSequence.JPG -------------------------------------------------------------------------------- /docs/README-HighLevelSystemView.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-HighLevelSystemView.JPG -------------------------------------------------------------------------------- /docs/README-RunCycles.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-RunCycles.JPG -------------------------------------------------------------------------------- /docs/README-TestEnvironment.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-TestEnvironment.JPG -------------------------------------------------------------------------------- /docs/README-UltraDict.md: -------------------------------------------------------------------------------- 1 | # UltraDict Installation 2 | 3 | [UltraDict](https://github.com/ronny-rentner/UltraDict) is a new package that is currently under development. The package provides a way for different running processes to share memory using the Python dictionary as both a storage metaphor and high level object interface definition. That is, it works justs like a Python dictionary (```{}``` or ```dict()```). 4 | 5 | The ```UltraDict``` Python package is not ```pip``` installable from the Internet repository. ```UltraDict``` plays well with ```gps_logger.gps_logger``` and ```obd_logger.obd_logger``` processes when all of the dependencies are met. 6 | 7 | ## Install Dependencies 8 | 9 | Some dependencies may be problematic to install on Windows and Mac. These inststructions should work on all 64 bit Debian and Debian based Linux versions in sync with (Debian) Bookworm Version 12.2 or newer. 10 | 11 | ```bash 12 | # UltraDict Documented Dependencies 13 | sudo apt-get install -y cmake 14 | python3.11 -m pip install --user pyrtcm 15 | python3.11 -m pip install --user atomics 16 | python3.11 -m pip install --user psutil 17 | python3.11 -m pip install --user ultraimport 18 | ``` 19 | 20 | ## Install UltraDict 21 | 22 | Here is how you access the development branch and install ```UltraDict``` on Linux. I haven't taken the time to install on Windows (missing certain Microsoft development tools) and I'm not even trying to figure out what can be done on Macs. These instructions assume that you have already downloaded Python 3.11 source code and then built and installed Python 3.11 from that code. You need the development tools from that build. 23 | 24 | The following assumes that the current version is ```0.0.6```. 25 | 26 | ```bash 27 | cd 28 | git clone https://github.com/thatlarrypearson/UltraDict.git 29 | cd UltraDict 30 | python3.11 -m build . 31 | python3.11 -m pip install dist/UltraDict-0.0.6-py3-none-any.whl 32 | ``` 33 | 34 | This build may take some time on a Raspberry Pi. 35 | 36 | ## Diagnosing UltraDict Related Problems 37 | 38 | When using ```UltraDict```, the most embarrassing **bug** to find is the one where ```--shared_dictionary_name``` is set in the consuming application (e.g. ```telemetry_obd.obd_logger```) but GPS or weather data just isn't showing up. When the expected data isn't showing up, add one or more of the following to the command line of ```telemetry_obd.obd_logger```: 39 | 40 | - ```--shared_dictionary_command_list``` 41 | - ```--gps_defaults``` 42 | - ```--wthr_defaults``` 43 | - ```--imu_defaults``` 44 | 45 | Ask me how I know. :unamused: 46 | 47 | ```UltraDict``` can be difficult to install on some systems such as **Windows 10**. This library will work without UltraDict installed. However, there will be a log message on startup whenever the ```--shared_dictionary_name``` command line argument is used as shown below. 48 | 49 | ```powershell 50 | PS C:\Users\human\src\telemetry-wthr> python3.11 -m gps_logger.gps_logger --shared_dictionary_name gps 51 | ERROR:gps_logger:import error: Shared Dictionary (gps) feature unsupported: UltraDict Not installed. 52 | ... 53 | ... 54 | ... 55 | ``` 56 | 57 | ## How To Know When Shared Memory Is/Isn't Working 58 | 59 | The following provides a method to verify that the shared memory has been created and available for use. These commands work on most Linux based computers including Raspberry Pi's running Raspberry Pi OS. These commands were _discovered_ while debugging a shared memory problem. You will see that the only data being shared was input by hand into the Python REPL. 60 | 61 | 62 | The first command gets a list of processes (```ps```) and sends its output to a filter (```grep```) that only passes through processes that contain the string ```python3.11```. The first filter passes its output onto another filter (```-v```) that removes all output lines that contain ```grep```. This results in process (```ps```) information that only includes process names with ```python3.11``` in them. 63 | 64 | The second field in the process output is the process ID number. This number (```384572```) is unique to an individual process running on the system. 65 | 66 | Using the process ID number (```384572```), the process memory map command (```pmap```) is used to get that specific process's shared memory (```-x```) information. We use the filter (```grep```) to pull out the lines that contain the shared dictionary name (```--shared_dictionary_name```) command line parameter (```GPS```) in a case independent way (```-i```). 67 | 68 | The result is two memory mapped regions supporting the shared dictionary between ```gps_logger``` and other running processes like ```gps_logger```. 69 | 70 | 71 | ```bash 72 | lbp@telemetry2:~ $ python3.11 73 | Python 3.11.6 (main, Oct 12 2023, 16:09:12) [GCC 12.2.0] on linux 74 | Type "help", "copyright", "credits" or "license" for more information. 75 | >>> from tcounter.common import ( 76 | ... default_shared_gps_command_list as SHARED_DICTIONARY_COMMAND_LIST, 77 | ... SharedDictionaryManager, 78 | ... BASE_PATH 79 | ... ) 80 | >>> sdm = SharedDictionaryManager('TELEMETRY') 81 | >>> for thing in sdm: 82 | ... print(thing) 83 | ... 84 | >>> sdm['thing'] = 'another thing' 85 | >>> for thing in sdm: 86 | ... print(thing) 87 | ... 88 | thing 89 | >>> 90 | lbp@telemetry2:~$ 91 | ``` 92 | 93 | Get a list of all the telemetry related processes using ```ps```, a Linux utility program providing basic process info. Narrow the search by using ```grep``` to filter the ouput to just list processes running Python 3.11. The process ID (**1713**), the number in the second column of output below, is needed in the following steps. 94 | 95 | ```bash 96 | lbp@telemetry2:~$ ps -eaf | grep python3.11 | grep -v grep 97 | lbp 1713 1681 0 14:34 ? 00:00:00 /home/lbp/.local/bin/python3.11 -m gps_logger.gps_logger --verbose --shared_dictionary_name TELEMETRY /home/lbp/telemetry-data/data 98 | lbp 2129 1739 5 14:35 ? 00:01:59 /home/lbp/.local/bin/python3.11 -m telemetry_obd.obd_logger --timeout 4.0 --no_fast --config_dir /home/lbp/telemetry-data/config --full_cycles 10000 --shared_dictionary_name TELEMETRY --gps_defaults /home/lbp/telemetry-data/data 99 | lbp 2441 2422 0 14:38 pts/2 00:00:00 python3.11 100 | 101 | lbp@telemetry2:~$ 102 | ``` 103 | 104 | Get a list of all the ```TELEMETRY``` related shared memory resources using ```pmap```, a Linux utility program. ```pmap``` needs the process ID (**1713**) from the previous step. Again, ```grep``` is used to filter the results to just the shared memory related to **TELEMETRY**, the name I use for shared memory in Telemetry related programs. 105 | 106 | ```bash 107 | lbp@telemetry2:~$ sudo pmap -x 1713 | grep TELEMETRY 108 | 1713: /home/lbp/.local/bin/python3.11 -m gps_logger.gps_logger --verbose --shared_dictionary_name TELEMETRY /home/lbp/telemetry-data/data 109 | 0000007fa36e0000 1024 0 0 rw-s- TELEMETRY_memory 110 | 0000007fa5768000 12 0 0 rw-s- TELEMETRY_register_memory 111 | 0000007fa576b000 4 4 4 rw-s- TELEMETRY_register 112 | 0000007fa576c000 4 4 4 rw-s- TELEMETRY 113 | lbp@telemetry2:~$ 114 | ``` 115 | 116 | Shared memory segments are represented within the Linux file system. This is useful showing shared memory segments and access permissions in the first column. The Linux ```ls -l``` command represents file permissions using the first column which contains 10 subfields and each character (including the dash or ```-```) is a field. 117 | 118 | - 1st character: ```-``` means not a directory. 119 | - 2nd through 4th characters are owner permissions. ```r``` is read, ```w``` is write and ```-``` is not executable. 120 | - 5th through 7th is group permissions. There are no group permissions. 121 | - 8th through 10th are everybody permissions. There are no everybody permissions. 122 | 123 | Conclusion! Only user ```lbp``` can read and write into these shared memory segments. This is **GOOD**. 124 | 125 | ```bash 126 | root@telemetry2:~# ls -l /dev/shm/TELEMETRY* 127 | total 20 128 | -rw------- 1 lbp dialout 1000 Oct 31 14:34 TELEMETRY 129 | -rw------- 1 lbp dialout 1048576 Oct 31 14:42 TELEMETRY_memory 130 | -rw------- 1 lbp dialout 1000 Oct 31 14:34 TELEMETRY_register 131 | -rw------- 1 lbp dialout 10000 Oct 31 14:34 TELEMETRY_register_memory 132 | 133 | root@telemetry2:~# cat /dev/shm/TELEMETRY 134 | -11 135 | 136 | root@telemetry2:~# cat /dev/shm/TELEMETRY_register 137 | 1 138 | 139 | root@telemetry2:~# cat /dev/shm/TELEMETRY_register_memory 140 | 141 | root@telemetry2:~# cat /dev/shm/TELEMETRY_memory | strings 142 | thing 143 | another thing 144 | 145 | root@telemetry2:~# 146 | 147 | root@telemetry2:~# lsof /dev/shm/TELEMETRY_memory 148 | lsof: WARNING: can't stat() fuse.gvfsd-fuse file system /run/user/1000/gvfs 149 | Output information may be incomplete. 150 | lsof: WARNING: can't stat() fuse.portal file system /run/user/1000/doc 151 | Output information may be incomplete. 152 | COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME 153 | python3.1 1713 lbp mem REG 0,22 1048576 12 /dev/shm/TELEMETRY_memory 154 | python3.1 1713 lbp 6u REG 0,22 1048576 12 /dev/shm/TELEMETRY_memory 155 | python3.1 1713 lbp 7u REG 0,22 1048576 12 /dev/shm/TELEMETRY_memory 156 | python3.1 2129 lbp mem REG 0,22 1048576 12 /dev/shm/TELEMETRY_memory 157 | python3.1 2129 lbp 5u REG 0,22 1048576 12 /dev/shm/TELEMETRY_memory 158 | python3.1 2129 lbp 6u REG 0,22 1048576 12 /dev/shm/TELEMETRY_memory 159 | python3.1 2441 lbp mem REG 0,22 1048576 12 /dev/shm/TELEMETRY_memory 160 | python3.1 2441 lbp 5u REG 0,22 1048576 12 /dev/shm/TELEMETRY_memory 161 | python3.1 2441 lbp 6u REG 0,22 1048576 12 /dev/shm/TELEMETRY_memory 162 | 163 | root@telemetry2:~# 164 | ``` 165 | 166 | 167 | ### Why ```ipcs``` Doesn't Work Anymore 168 | 169 | ```bash 170 | lbp@telemetry2:~# sudo ipcs -m 171 | 172 | ------ Shared Memory Segments -------- 173 | key shmid owner perms bytes nattch status 174 | ``` 175 | 176 | [```ipcs``` doesn't show my shared memory and semaphores](https://stackoverflow.com/questions/15660812/ 177 | ipcs-doesnt-show-my-shared-memory-and-semaphores) 178 | 179 | ```ipcs``` uses the UNIX System V Release 4 shared memory model. I used that software in the 1980's. UltraDict uses the modern Linux model for shared memory. Much better. 180 | 181 | ## LICENSE 182 | 183 | [MIT License](../LICENSE.md) 184 | -------------------------------------------------------------------------------- /docs/README-rpi-gui-bt-devices-authenticated.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-rpi-gui-bt-devices-authenticated.jpg -------------------------------------------------------------------------------- /docs/README-rpi-gui-bt-devices-connect-failed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-rpi-gui-bt-devices-connect-failed.jpg -------------------------------------------------------------------------------- /docs/README-rpi-gui-bt-devices-connect.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-rpi-gui-bt-devices-connect.jpg -------------------------------------------------------------------------------- /docs/README-rpi-gui-bt-devices-dialog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-rpi-gui-bt-devices-dialog.jpg -------------------------------------------------------------------------------- /docs/README-rpi-gui-bt-devices-search-pair.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-rpi-gui-bt-devices-search-pair.jpg -------------------------------------------------------------------------------- /docs/README-rpi-gui-bt-devices-search-pairing-authentication.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-rpi-gui-bt-devices-search-pairing-authentication.jpg -------------------------------------------------------------------------------- /docs/README-rpi-gui-bt-devices-search-pairing-request.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-rpi-gui-bt-devices-search-pairing-request.jpg -------------------------------------------------------------------------------- /docs/README-rpi-gui-bt-devices-search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-rpi-gui-bt-devices-search.jpg -------------------------------------------------------------------------------- /docs/README-rpi-gui-bt-devices-setup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-rpi-gui-bt-devices-setup.jpg -------------------------------------------------------------------------------- /docs/README-rpi-gui-bt-devices-trust.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-rpi-gui-bt-devices-trust.jpg -------------------------------------------------------------------------------- /docs/README-rpi-gui-bt-devices-trusted.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-rpi-gui-bt-devices-trusted.jpg -------------------------------------------------------------------------------- /docs/README-rpi-gui-bt-devices.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-rpi-gui-bt-devices.jpg -------------------------------------------------------------------------------- /docs/README-rpi-gui-bt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatlarrypearson/telemetry-obd/450671b412e6f5fc5a017f7502d984c7d4f9b056/docs/README-rpi-gui-bt.jpg -------------------------------------------------------------------------------- /docs/python-OBD-Install.md: -------------------------------------------------------------------------------- 1 | # ```python-OBD``` Package Install 2 | 3 | ```python-OBD``` is a Python package handling realtime sensor data from OBD vehicle interfaces. Works with ELM327 OBD-II compliant adapters and runs on the Raspberry Pi without modification with Python 3.8. 4 | 5 | However, ```python-OBD``` needs to run on Python 3.10 to take advantage of other third party Python packages like [UltraDict](https://github.com/ronny-rentner/UltraDict) and the newest versions of [pint](https://github.com/hgrecco/pint). 6 | 7 | ## Get The Source Code 8 | 9 | ```bash 10 | human@hostname:~ $ # Install python-OBD from source (github repository) 11 | human@hostname:~ $ git clone https://github.com/brendan-w/python-OBD.git 12 | ``` 13 | 14 | ## Check Dependency Requirements 15 | 16 | ```bash 17 | human@hostname:~ $ # Check to see what version of Pint is specified in dependency requirements 18 | human@hostname:~ $ cd python-OBD 19 | human@hostname:~/python-OBD $ grep pint setup.py 20 | install_requires=["pyserial==3.*", "pint==0.7.*"], 21 | human@hostname:~/python-OBD $ 22 | ``` 23 | 24 | The above dependency requirements as specified by the ```install_requires``` keyword in the ```setup.py``` file show that any ```pint``` version starting with ```0.7.``` can be used. During ```pip``` install, the ```pint``` version used will be ```0.7.2```. 25 | 26 | If the dependency requirements show pint version as ```0.7.*```, Python 3.10 *will not* work. 27 | 28 | If the dependency requirements show any version of pint before ```0.19.2```, Python 3.10 *may not work*. 29 | 30 | These packages *WILL NOT WORK* without Python 3.10 and Pint 0.19.2: 31 | 32 | - [telemetry-obd](https://github.com/thatlarrypearson/telemetry-obd) 33 | - [telemetry-obd-log-to-csv](https://github.com/thatlarrypearson/telemetry-obd-log-to-csv) 34 | - [telemetry-gps](https://github.com/thatlarrypearson/telemetry-gps) 35 | 36 | ## Change Dependency Requirements 37 | 38 | Using a code editor like [Thonny Python IDE](https://thonny.org/) or [Visual Studio Code](https://code.visualstudio.com/), change ```0.7.*``` to ```0.19.2``` in ```setup.py```. 39 | 40 | ```bash 41 | human@hostname:~/python-OBD $ # Check your work 42 | human@hostname:~/python-OBD $ grep pint setup.py 43 | install_requires=["pyserial==3.*", "pint==0.19.2"], 44 | human@hostname:~/python-OBD $ 45 | ``` 46 | 47 | ## Install Using Necessary Dependency Requirements 48 | 49 | ```bash 50 | human@hostname:~/python-OBD $ # Build and install OBD package 51 | human@hostname:~/python-OBD $ python3.10 -m build 52 | human@hostname:~/python-OBD $ python3.10 -m pip install --user dist/obd-0.7.1-py3-none-any.whl 53 | ``` 54 | 55 | ## Test Installation 56 | 57 | ```bash 58 | human@hostname:~/python-OBD $ python3.10 59 | Python 3.10.4 (main, Apr 11 2022, 15:49:38) [GCC 10.2.1 20210110] on linux 60 | Type "help", "copyright", "credits" or "license" for more information. 61 | >>> import obd 62 | >>> exit 63 | Use exit() or Ctrl-D (i.e. EOF) to exit 64 | >>> exit() 65 | human@hostname:~/python-OBD $ 66 | ``` 67 | 68 | ## LICENSE 69 | 70 | [MIT License](../LICENSE.md) 71 | -------------------------------------------------------------------------------- /etc/rc.local: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # 3 | # rc.local 4 | # 5 | # This script is executed at the end of each multiuser runlevel. 6 | # Make sure that the script will "exit 0" on success or any other 7 | # value on error. 8 | # 9 | # In order to enable or disable this script just change the execution 10 | # bits. 11 | # 12 | # By default this script does nothing. 13 | 14 | # Print the IP address 15 | _IP=$(hostname -I) || true 16 | if [ "$_IP" ]; then 17 | printf "My IP address is %s\n" "$_IP" 18 | fi 19 | 20 | # BEGIN TELEMETRY SUPPORT 21 | 22 | if [ -x "/root/bin/telemetry.rc.local.counter" ] 23 | then 24 | /bin/nohup "/root/bin/telemetry.rc.local.counter" & 25 | fi 26 | 27 | if [ -x "/root/bin/telemetry.rc.local.gps" ] 28 | then 29 | /bin/nohup "/root/bin/telemetry.rc.local.gps" & 30 | fi 31 | 32 | if [ -x "/root/bin/telemetry.rc.local.imu" ] 33 | then 34 | /bin/nohup "/root/bin/telemetry.rc.local.imu" & 35 | fi 36 | 37 | if [ -x "/root/bin/telemetry.rc.local.wthr" ] 38 | then 39 | /bin/nohup "/root/bin/telemetry.rc.local.wthr" & 40 | fi 41 | 42 | if [ -x "/root/bin/telemetry.rc.local.obd" ] 43 | then 44 | /bin/nohup "/root/bin/telemetry.rc.local.obd" & 45 | fi 46 | 47 | # END TELEMETRY SUPPORT 48 | 49 | exit 0 50 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [metadata] 6 | name = "telemetry-obd" 7 | url = "https://github.com/thatlarrypearson/telemetry-obd" 8 | version = "0.5.0" 9 | description = "Telemetry OBD Data Logger" 10 | long_description = "file:README.md" 11 | authors = [ 12 | "Larry Pearson ", 13 | ] 14 | license = "MIT" 15 | readme = "README.md" 16 | python = "3.11" 17 | homepage = "https://github.com/thatlarrypearson/telemetry-obd" 18 | repository = "https://github.com/thatlarrypearson/telemetry-obd" 19 | documentation = "https://github.com/thatlarrypearson/telemetry-obd" 20 | keywords = ["obd", "telematics", "vehicle"] 21 | dependencies = [ 22 | 'obd == 0.7.2', 23 | 'pint == 0.20.1', 24 | 'rich >= 10.12.0', 25 | 'obd == 0.7.2', 26 | 'telemetry-counter == 0.5.0', 27 | ] 28 | 29 | classifiers = [ 30 | "Development Status :: 4 - Beta", 31 | "Intended Audience :: Developers", 32 | "License :: OSI Approved :: MIT License", 33 | "Operating System :: OS Independent", 34 | "Natural Language :: English", 35 | "Programming Language :: Python :: 3 :: Only", 36 | "Programming Language :: Python :: 3.10", 37 | "Programming Language :: Python :: 3.11", 38 | "Programming Language :: Python :: Implementation :: CPython", 39 | "Topic :: Software Development :: Libraries :: Application Frameworks", 40 | "Topic :: Software Development :: Libraries :: Python Modules", 41 | ] 42 | 43 | 44 | -------------------------------------------------------------------------------- /root/bin/telemetry.rc.local.obd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | # 3 | # telemetry.rc.local.obd - This script is executed by the system /etc/rc.local script on system boot 4 | 5 | export OBD_USER="lbp" 6 | export OBD_GROUP="dialout" 7 | export OBD_HOME="/home/${OBD_USER}" 8 | export DEBUG="True" 9 | export TMP_DIR="/root/tmp" 10 | export LOG_FILE="${TMP_DIR}/telemetry-obd_$(date '+%Y-%m-%d_%H:%M:%S').log" 11 | 12 | rm -f /tmp/rfcomm-bind /tmp/btctl-connect 13 | 14 | if [ ! -d "${TMP_DIR}" ] 15 | then 16 | mkdir --parents "${TMP_DIR}" 17 | fi 18 | 19 | # redirect all stdout and stderr to file 20 | exec &> "${LOG_FILE}" 21 | 22 | # Debugging support 23 | if [ "${DEBUG}" = "True" ] 24 | then 25 | # enable shell debug mode 26 | set -x 27 | fi 28 | 29 | # turn off stdin 30 | 0<&- 31 | 32 | # Enable Bluetooth subsystem 33 | bluetoothctl power on 34 | bluetoothctl agent on 35 | 36 | # Give Bluetooth subsystem more time to activate 37 | sleep 45 38 | 39 | ## Bind the available paired OBDII device to /dev/rfcomm0. 40 | ## Change the Bluetooth MAC addresses in the next line to match your addresses. 41 | ## One or more MAC addresses matching available Bluetooth OBD devices are required. 42 | ## The following tries MAC addresses until a working one is found. 43 | ## To make more than one attempt at each/any Bluetooth MAC address, duplicate the address(es) as shown below. 44 | export RtnCode=1 45 | for BT_MAC_ADDR in "00:04:3E:5A:A7:67" "00:19:5D:26:4B:5F" "00:04:3E:5A:A7:67" "00:19:5D:26:4B:5F" "00:04:3E:5A:A7:67" "00:19:5D:26:4B:5F" 46 | do 47 | bluetoothctl connect "${BT_MAC_ADDR}" 2>&1 >> /tmp/btctl-connect 48 | grep "Connected: yes" /tmp/btctl-connect 49 | RtnCode="$?" 50 | 51 | if [ "${RtnCode}" -eq 0 ] 52 | then 53 | rfcomm bind rfcomm0 "${BT_MAC_ADDR}" 2>&1 >> /tmp/rfcomm-bind 54 | break 55 | fi 56 | echo "${BT_MAC_ADDR}" '##########################' >> /tmp/rfcomm-bind 57 | echo "${BT_MAC_ADDR}" '##########################' >> /tmp/btctl-connect 58 | sleep 5 59 | done 60 | 61 | if [ "${RtnCode}" -ne 0 ] 62 | then 63 | echo Exiting. Unable to bind serial device to Bluetooth connection. obd_logger.sh not starting. 64 | exit 0 65 | fi 66 | 67 | echo Ready to start obd_logger.sh 68 | 69 | ## Run the script obd_logger.sh as user "${OBD_USER}" and group "${OBD_GROUP}" 70 | runuser -u "${OBD_USER}" -g dialout "${OBD_HOME}/telemetry-obd/bin/obd_logger.sh" & 71 | 72 | exit 0 73 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [check-manifest] 2 | ignore = 3 | *.sublime-project 4 | .git* 5 | .vscode/* 6 | manage.py 7 | data/* 8 | tmp/* 9 | examples/* 10 | stuff/* 11 | docs/* 12 | .mypy* 13 | .pytest* 14 | __pycache__* 15 | 16 | 17 | [metadata] 18 | name = telemetry-obd 19 | author = Larry Pearson 20 | author_email = not-an-email-address@gmail.com 21 | version = 0.5.0 22 | description = Telemetry Onboard Diagnostic Data Logger. 23 | long_description = file:README.md 24 | long_description_content_type = text/markdown 25 | url = https://github.com/thatlarrypearson?tab=repositories 26 | license = MIT 27 | classifiers = 28 | Development Status :: 4 - Beta 29 | Environment :: Web Environment 30 | Intended Audience :: Developers 31 | License :: OSI Approved :: MIT License 32 | Operating System :: OS Independent 33 | Natural Language :: English 34 | Programming Language :: Python :: 3 :: Only 35 | Programming Language :: Python :: 3.10 36 | Programming Language :: Python :: 3.11 37 | Programming Language :: Python :: Implementation :: CPython 38 | Topic :: Software Development :: Libraries :: Application Frameworks 39 | Topic :: Software Development :: Libraries :: Python Modules 40 | 41 | [options] 42 | include_package_data = true 43 | python_requires = >=3.10 44 | setup_requires = 45 | setuptools >= 38.3.0 46 | install_requires = 47 | obd == 0.7.2 48 | pint == 0.20.1 49 | rich >= 10.12.0 50 | obd == 0.7.2 51 | telemetry-counter == 0.5.0 52 | packages = find: 53 | -------------------------------------------------------------------------------- /telemetry_obd/__init__.py: -------------------------------------------------------------------------------- 1 | """for now, nothing to init.""" 2 | __version__ = "0.5.0" 3 | -------------------------------------------------------------------------------- /telemetry_obd/list_all_commands.py: -------------------------------------------------------------------------------- 1 | # OBD List All Commands 2 | # telemetry-obd/telemetry_obd/list_all_commands.py 3 | """ 4 | Lists every known OBD command with relevant info. 5 | """ 6 | import csv 7 | import logging 8 | from sys import stdout 9 | from rich.console import Console 10 | from rich.table import Table 11 | from obd.commands import __mode1__, __mode9__ 12 | from argparse import ArgumentParser 13 | from .add_commands import NEW_COMMANDS 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | def get_command_list() -> dict: 18 | """ 19 | Return dictionary of commands. 20 | """ 21 | return_value = {cmd.name: { 22 | 'command': cmd.name, 23 | 'description': cmd.desc, 24 | 'source': '__mode1__', 25 | 'mode': cmd.command.decode('utf-8')[:2], 26 | 'pid': cmd.command.decode('utf-8')[2:4], 27 | } for cmd in __mode1__} 28 | 29 | for cmd in __mode9__: 30 | return_value[cmd.name] = { 31 | 'command': cmd.name, 32 | 'description': cmd.desc, 33 | 'source': '__mode9__', 34 | 'mode': cmd.command.decode('utf-8')[:2], 35 | 'pid': cmd.command.decode('utf-8')[2:4], 36 | } 37 | 38 | for cmd in NEW_COMMANDS: 39 | return_value[cmd.name] = { 40 | 'command': cmd.name, 41 | 'description': cmd.desc, 42 | 'source': 'NEW_COMMANDS', 43 | 'mode': cmd.command.decode('utf-8')[:2], 44 | 'pid': cmd.command.decode('utf-8')[2:4], 45 | } 46 | 47 | return return_value 48 | 49 | def sort_dict_keys(dict_thing:dict) -> list: 50 | """ 51 | sort dictionary keys 52 | """ 53 | return_value = [key for key in dict_thing] 54 | return sorted(return_value) 55 | 56 | def argument_parsing()-> dict: 57 | """Argument parsing""" 58 | parser = ArgumentParser(description="Telemetry OBD List All Commands") 59 | parser.add_argument( 60 | "--csv", 61 | help="Output in CSV format.", 62 | default=False, 63 | action='store_true' 64 | ) 65 | 66 | return vars(parser.parse_args()) 67 | 68 | def csv_print(output:dict, output_file=stdout): 69 | field_names = [ 70 | 'command', 71 | 'description', 72 | 'source', 73 | 'mode', 74 | 'pid', 75 | ] 76 | writer = csv.DictWriter(output_file, fieldnames=field_names) 77 | writer.writeheader() 78 | 79 | for command_name in output: 80 | writer.writerow(output[command_name]) 81 | 82 | def rich_print(output:dict): 83 | console = Console() 84 | 85 | table = Table(show_header=True, header_style="bold magenta") 86 | table.add_column("OBD Command", justify='left') 87 | table.add_column("Description", justify='left') 88 | table.add_column("Source", justify='left') 89 | table.add_column("Mode") 90 | table.add_column("PID") 91 | 92 | for command_name in output: 93 | table.add_row( 94 | output[command_name]['command'], 95 | output[command_name]['description'], 96 | output[command_name]['source'], 97 | f"0x{output[command_name]['mode']}", 98 | f"0x{output[command_name]['pid']}", 99 | ) 100 | 101 | table.add_row( 102 | '[bold red]Total Commands[/bold red]', 103 | f"[bold red]{len(output)}[/bold red]" 104 | '', 105 | '', 106 | '', 107 | ) 108 | 109 | 110 | console.print(table) 111 | 112 | 113 | def main(): 114 | """Run main function.""" 115 | 116 | args = argument_parsing() 117 | 118 | csv = args['csv'] 119 | 120 | command_list = get_command_list() 121 | command_names = sort_dict_keys(command_list) 122 | output = {command_name: { 123 | 'command': command_name, 124 | 'description': command_list[command_name]['description'], 125 | 'source': command_list[command_name]['source'], 126 | 'mode': command_list[command_name]['mode'], 127 | 'pid': command_list[command_name]['pid'], 128 | } for command_name in command_names} 129 | 130 | if csv: 131 | csv_print(output) 132 | else: 133 | rich_print(output) 134 | 135 | 136 | if __name__ == "__main__": 137 | main() 138 | 139 | -------------------------------------------------------------------------------- /telemetry_obd/obd_command_tester.py: -------------------------------------------------------------------------------- 1 | # OBD Command Tester 2 | # telemetry-obd/telemetry_obd/obd_command_tester.py 3 | """ 4 | Tests every known OBD command against an OBD interface. 5 | """ 6 | 7 | from obd.commands import __mode1__, __mode9__ 8 | from datetime import datetime, timezone 9 | from pathlib import Path 10 | from pint import OffsetUnitCalculusError 11 | from argparse import ArgumentParser 12 | from sys import stdout, stderr 13 | from traceback import print_exc 14 | 15 | import sys 16 | import json 17 | import logging 18 | import obd 19 | from tcounter.common import ( 20 | get_output_file_name, 21 | get_next_application_counter_value, 22 | BASE_PATH, 23 | ) 24 | from .obd_common_functions import ( 25 | load_custom_commands, 26 | get_vin_from_vehicle, 27 | get_elm_info, 28 | clean_obd_query_response, 29 | get_obd_connection, 30 | execute_obd_command, 31 | ) 32 | from .add_commands import NEW_COMMANDS 33 | 34 | logger = logging.getLogger(__name__) 35 | 36 | CONNECTION_WAIT_DELAY = 15.0 37 | CYCLE_COUNT = 40 38 | TIMEOUT=0.5 39 | 40 | logger = logging.getLogger(__name__) 41 | 42 | def get_command_list() -> list: 43 | """ 44 | Return list of all available OBD commands. 45 | """ 46 | return [cmd.name for cmd in __mode1__ + __mode9__ + NEW_COMMANDS] 47 | 48 | def argument_parsing()-> dict: 49 | """Argument parsing""" 50 | parser = ArgumentParser(description="Telemetry OBD Command Tester") 51 | parser.add_argument( 52 | "base_path", 53 | nargs='?', 54 | metavar="base_path", 55 | default=[BASE_PATH, ], 56 | help=f"Relative or absolute output data directory. Defaults to '{BASE_PATH}'." 57 | ) 58 | parser.add_argument( 59 | '--cycles', 60 | type=int, 61 | default=CYCLE_COUNT, 62 | help=( 63 | "The number of cycles before ending. A cycle consists of all known OBD commands." + 64 | f" Default is {CYCLE_COUNT}." 65 | ) 66 | ) 67 | parser.add_argument( 68 | '--timeout', 69 | type=float, 70 | default=TIMEOUT, 71 | help=( 72 | "The number seconds before a command times out." + 73 | f" Default is {TIMEOUT} seconds." 74 | ) 75 | ) 76 | parser.add_argument( 77 | "--logging", 78 | help="Turn on logging in python-obd library. Default is off.", 79 | default=False, 80 | action='store_true' 81 | ) 82 | parser.add_argument( 83 | "--no_fast", 84 | help="When on, commands for every request will be unaltered with potentially long timeouts " + 85 | "when the car doesn't respond promptly or at all. " + 86 | "When off (fast is on), commands are optimized before being sent to the car. A timeout is " + 87 | "added at the end of the command. Default is off so fast is on. ", 88 | default=False, 89 | action='store_true' 90 | ) 91 | parser.add_argument( 92 | "--verbose", 93 | help="Turn verbose output on. Default is off.", 94 | default=False, 95 | action='store_true' 96 | ) 97 | return vars(parser.parse_args()) 98 | 99 | def main(): 100 | """Run main function.""" 101 | 102 | args = argument_parsing() 103 | 104 | base_path = args['base_path'] 105 | fast = not args['no_fast'] 106 | timeout = args['timeout'] 107 | verbose = args['verbose'] 108 | cycles = args['cycles'] 109 | 110 | logging_level = logging.WARNING 111 | 112 | if verbose: 113 | logging_level = logging.INFO 114 | 115 | if args['logging']: 116 | logging_level = logging.DEBUG 117 | 118 | logging.basicConfig(stream=sys.stdout, level=logging_level) 119 | obd.logger.setLevel(logging_level) 120 | 121 | logging.info(f"argument --fast: {fast}") 122 | logging.info(f"argument --timeout: {timeout}") 123 | logging.info(f"argument --verbose: {verbose}") 124 | logging.info(f"argument --cycles: {cycles}") 125 | logging.info(f"argument --logging: {args['logging']} ") 126 | logging.debug("debug logging enabled") 127 | 128 | connection = get_obd_connection(fast=fast, timeout=timeout) 129 | 130 | elm_version, elm_voltage = get_elm_info(connection) 131 | logging.info("ELM VERSION: {elm_version}, ELM VOLTAGE: {elm_voltage}") 132 | 133 | custom_commands = load_custom_commands(connection) 134 | 135 | vin = get_vin_from_vehicle(connection) 136 | logging.info(f"VIN: {vin}") 137 | 138 | base_path = args['base_path'] 139 | 140 | output_file_path = get_output_file_name('obd-cmd-test', base_path=base_path, vin=vin) 141 | logging.info(f"output file: {output_file_path}") 142 | 143 | try: 144 | with open(output_file_path, mode='x', encoding='utf-8') as out_file: 145 | for cycle in range(cycles): 146 | logging.info(f"cycle {cycle} in {cycles}") 147 | for command_name in get_command_list(): 148 | logging.info(f"command_name {command_name}") 149 | 150 | iso_ts_pre = datetime.isoformat( 151 | datetime.now(tz=timezone.utc) 152 | ) 153 | 154 | try: 155 | 156 | obd_response = execute_obd_command(connection, command_name) 157 | 158 | except OffsetUnitCalculusError as e: 159 | logging.exception(f"Exception: {e.__class__.__name__}: {e}") 160 | logging.exception(f"OffsetUnitCalculusError on {command_name}, decoder must be fixed") 161 | logging.exception(f"Exception: {e}") 162 | 163 | except Exception as e: 164 | logging.exception(f"Exception: {e}") 165 | if not connection.is_connected(): 166 | logging.error(f"connection failure on {command_name}, reconnecting") 167 | connection.close() 168 | connection = get_obd_connection(fast=fast, timeout=timeout) 169 | 170 | iso_ts_post = datetime.isoformat( 171 | datetime.now(tz=timezone.utc) 172 | ) 173 | 174 | obd_response_value = clean_obd_query_response(command_name, obd_response) 175 | 176 | logging.info(f"saving: {command_name}, {obd_response_value}, {iso_ts_pre}, {iso_ts_post}") 177 | 178 | out_file.write(json.dumps({ 179 | 'command_name': command_name, 180 | 'obd_response_value': obd_response_value, 181 | 'iso_ts_pre': iso_ts_pre, 182 | 'iso_ts_post': iso_ts_post, 183 | }) + "\n" 184 | ) 185 | 186 | except FileExistsError: 187 | logger.error(f"open(): FileExistsError: {output_file_path}") 188 | imu_counter = get_next_application_counter_value('obd-cmd-test') 189 | logger.error(f"get_log_file_handle(): Incremented 'obd-cmd-test' counter to {imu_counter}") 190 | 191 | 192 | if __name__ == "__main__": 193 | main() 194 | 195 | -------------------------------------------------------------------------------- /telemetry_obd/obd_common_functions.py: -------------------------------------------------------------------------------- 1 | """telemetry_obd/obd_common_functions.py: Common OBD functions.""" 2 | 3 | from time import sleep 4 | from typing import List 5 | from datetime import datetime, timezone 6 | import logging 7 | import configparser 8 | import obd 9 | from pint import UnitRegistry 10 | from obd.utils import BitArray 11 | from obd.codes import BASE_TESTS 12 | from obd.OBDResponse import Status 13 | from .add_commands import NEW_COMMANDS, ureg 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | CONNECTION_WAIT_DELAY = 15.0 18 | CONNECTION_RETRY_COUNT = 5 19 | 20 | local_commands = { 21 | new_command.name: new_command for new_command in NEW_COMMANDS 22 | } 23 | 24 | OBD_ERROR_MESSAGES = { 25 | "ACT ALERT": "OBD adapter switching to low power mode in 1 minute.", 26 | "BUFFER FULL": "Incoming OBD message buffer overflow.", 27 | "BUS BUSY": "Send timeout occurred before bus became idle.", 28 | "BUS ERROR": "Potential OBD adapter to vehicle OBD interface circuit problem.", 29 | "CAN ERROR": "OBD adapter having trouble transmitting or receiving messages", 30 | ">DATA ERROR": "CRC/checksum error.", 31 | "DATA ERROR": "Data formatting error.", 32 | "FB ERROR": "Feedback error. Circuit problem?", 33 | "FC RX TIMEOUT": "Timeout error. Circuit problem?", 34 | "LP ALERT": "Low power alert. Standby mode in 2 seconds.", 35 | "LV RESET": "Low voltage reset. Vehicle power brownout condition?", 36 | "NO DATA": "No vehicle response before read timeout. Command not supported?", 37 | "OUT OF MEMORY": "Not enough RAM in adapter to complete operation.", 38 | "RX ERROR": "Received message garbled. Incorrect baud rate?", 39 | "STOPPED": "Adapter received character on UART interrupting current OBD command.", 40 | "UART RX OVERFLOW": "UART receive buffer overflow.", 41 | "UNABLE TO CONNECT": "OBD adapter unable to detect supported vehicle OBD protocol.", 42 | } 43 | 44 | class CommandNameGenerator(): 45 | """Iterator for providing a never ending list of OBD commands.""" 46 | 47 | # Three different cycles for running commands 48 | startup_names: List[str] = [] 49 | startup = None 50 | housekeeping_names: List[str] = [] 51 | housekeeping = None 52 | cycle_names: List[str] = [] 53 | cycle = None 54 | full_cycles_count = 0 55 | 56 | def __init__(self, settings_file: str): 57 | """Init function.""" 58 | self.settings_file = settings_file 59 | self.load_names() 60 | 61 | def load_names(self): 62 | """Load three sets of OBD command names.""" 63 | config = configparser.ConfigParser() 64 | config.read(self.settings_file) 65 | self.startup_names = (config['STARTUP NAMES']['startup']).split() 66 | self.startup = self.startup_names.__iter__() 67 | 68 | self.housekeeping_names = ( 69 | (config['HOUSEKEEPING NAMES']['housekeeping']).split() 70 | ) 71 | self.housekeeping = self.housekeeping_names.__iter__() 72 | 73 | self.cycle_names = (config['CYCLE NAMES']['cycle']).split() 74 | self.cycle = self.cycle_names.__iter__() 75 | 76 | def __iter__(self): 77 | """Start iterator.""" 78 | return self 79 | 80 | def __next__(self): 81 | """Get the next iterable.""" 82 | if self.startup: 83 | try: 84 | return self.startup.__next__() 85 | except StopIteration: 86 | self.startup = None 87 | 88 | if self.cycle: 89 | try: 90 | return self.cycle.__next__() 91 | except StopIteration: 92 | self.cycle = None 93 | 94 | if not self.housekeeping: 95 | self.housekeeping = self.housekeeping_names.__iter__() 96 | 97 | self.cycle = self.cycle_names.__iter__() 98 | 99 | try: 100 | return self.housekeeping.__next__() 101 | except StopIteration: 102 | self.housekeeping = None 103 | self.full_cycles_count += 1 104 | 105 | return self.__next__() 106 | 107 | 108 | def get_vin_from_vehicle(connection): 109 | """Get Vehicle Information Number (VIN) from vehicle.""" 110 | obd_response = connection.query(obd.commands["VIN"]) 111 | 112 | if obd_response and obd_response.value: 113 | return str(bytes(obd_response.value), 'utf-8') 114 | 115 | return 'UNKNOWN_VIN' 116 | 117 | def get_elm_info(connection): 118 | """Return ELM version and voltage""" 119 | version_response = connection.query(obd.commands["ELM_VERSION"]) 120 | voltage_response = connection.query(obd.commands["ELM_VOLTAGE"]) 121 | 122 | return str(version_response.value), str(voltage_response.value) 123 | 124 | 125 | def load_custom_commands(connection): 126 | """Load custom commands into a dictionary.""" 127 | custom_commands = {} 128 | for new_command in NEW_COMMANDS: 129 | logging.info(f"load_custom_commands(): {new_command.name}") 130 | if connection: 131 | connection.supported_commands.add(new_command) 132 | custom_commands[new_command.name] = new_command 133 | return custom_commands 134 | 135 | def tuple_to_list_converter(t:tuple)->list: 136 | """ 137 | Converts python-OBD/OBD/OBDCommand.py OBDCommand tuple output to list output. 138 | For example, O2_SENSORS value: "((), (False, False, False, False), (False, False, False, True))" 139 | gets converted to [False, False, False, False, False, False, False, True] 140 | """ 141 | return_value = [] 142 | for item in t: 143 | if isinstance(item, tuple) and len(item) == 0: 144 | continue 145 | elif isinstance(item, tuple): 146 | return_value += tuple_to_list_converter(item) 147 | elif isinstance(item, BitArray): 148 | for b in item: 149 | return_value.append(b) 150 | else: 151 | return_value.append(item) 152 | 153 | return return_value 154 | 155 | def list_cleaner(command_name:str, items:list)->list: 156 | return_value = [] 157 | for item in items: 158 | if ( 159 | isinstance(item, ureg.Quantity) or 160 | isinstance(item, bytearray) 161 | ): 162 | return_value.append(str(item)) 163 | elif 'Quantity' in item.__class__.__name__: 164 | return_value.append(str(item)) 165 | else: 166 | return_value.append(item) 167 | return return_value 168 | 169 | def clean_obd_query_response(command_name:str, obd_response): 170 | """ 171 | fixes problems in OBD connection.query responses. 172 | - is_null() True to "no response" 173 | - tuples to lists 174 | - byte arrays to strings 175 | - "NO DATA", "CAN ERROR", etc. to "no response" 176 | - None to "no response" 177 | - BitArray to list of True/False values 178 | - Status to serialized version of Status 179 | - pint Quantity object serialized by pint. 180 | """ 181 | if not obd_response: 182 | logging.debug(f"command_name {command_name}: obd_response is None") 183 | return None 184 | 185 | if obd_response.is_null() or obd_response.value is None: 186 | logging.debug(f"command_name {command_name}: obd_response.is_null or obd_response.value is None") 187 | return "no response" 188 | 189 | for message in obd_response.messages: 190 | for obd_error_message, obd_error_description in OBD_ERROR_MESSAGES.items(): 191 | raw_message = message.raw() 192 | if obd_error_message in raw_message: 193 | logging.error(f"command_name: {command_name}: OBD adapter message error: \"{obd_error_message}\": {obd_error_description}") 194 | return "no response" 195 | 196 | if isinstance(obd_response.value, bytearray): 197 | return obd_response.value.decode("utf-8") 198 | 199 | if isinstance(obd_response.value, BitArray): 200 | return list(obd_response.value) 201 | 202 | if isinstance(obd_response.value, Status): 203 | return [ 204 | str(obd_response.value.__dict__[base_test]) 205 | for base_test in BASE_TESTS 206 | ] 207 | 208 | if isinstance(obd_response.value, ureg.Quantity) or 'Quantity' in obd_response.value.__class__.__name__: 209 | return str(obd_response.value) 210 | 211 | if isinstance(obd_response.value, list): 212 | return list_cleaner(command_name, obd_response.value) 213 | 214 | if isinstance(obd_response.value, tuple): 215 | return tuple_to_list_converter(obd_response.value) 216 | 217 | return obd_response.value 218 | 219 | def get_obd_connection(fast:bool, timeout:float)->obd.OBD: 220 | """ 221 | return an OBD connection instance that connects to the first ELM 327 compatible device 222 | connected to any of the local serial ports. If no device found, exit program with error code 1. 223 | """ 224 | ports = sorted(obd.scan_serial()) 225 | 226 | logging.info(f"identified ports {ports}") 227 | 228 | for port in ports: 229 | logging.info(f"connecting to port {port}") 230 | 231 | try: 232 | 233 | # OBD(portstr=None, baudrate=None, protocol=None, fast=True, timeout=0.1, check_voltage=True) 234 | connection = obd.OBD(portstr=port, fast=fast, timeout=timeout) 235 | for t in range(1, CONNECTION_RETRY_COUNT): 236 | 237 | if connection.is_connected(): 238 | logging.info(f"connected to {port} on try {t} of {CONNECTION_RETRY_COUNT}") 239 | custom_commands = load_custom_commands(connection) 240 | return connection 241 | 242 | if connection.status() == obd.OBDStatus.NOT_CONNECTED: 243 | logging.warn(f"ELM 327 Adapter Not Found on {port}on try {t} of {CONNECTION_RETRY_COUNT}") 244 | break 245 | 246 | logging.info(f"Waiting for OBD Connection on {port} on try {t} of {CONNECTION_RETRY_COUNT}: {connection.status()}") 247 | 248 | sleep(CONNECTION_WAIT_DELAY) 249 | 250 | except Exception as e: 251 | logging.exception(f"OBD Connection on port {port} unavailable. Exception: {e}") 252 | 253 | logging.info(f"ELM 327 type device not found in devices: {ports}") 254 | logging.info("exiting...") 255 | 256 | exit(1) 257 | 258 | def recover_lost_connection(connection:obd.OBD, fast:bool, timeout:float)->obd.OBD: 259 | """ 260 | Recover lost connection and return a new working connection handle. 261 | """ 262 | logging.info("recovering lost connection") 263 | connection.close() 264 | sleep(CONNECTION_WAIT_DELAY) 265 | connection = get_obd_connection(fast=fast, timeout=timeout) 266 | 267 | return connection 268 | 269 | def execute_obd_command(connection:obd.OBD, command_name:str): 270 | """ 271 | executes OBD interface query given command_name on OBD connection. 272 | returns list or value 273 | """ 274 | if obd.commands.has_name(command_name): 275 | obd_response = connection.query(obd.commands[command_name], force=True) 276 | 277 | elif command_name in local_commands: 278 | obd_response = connection.query(local_commands[command_name], force=True) 279 | else: 280 | # raise LookupError(f"command <{command_name}> missing from python-obd and custom commands") 281 | logging.warn(f"LookupError: config file has command name <{command_name}> that doesn't exist") 282 | return None 283 | 284 | return obd_response 285 | 286 | -------------------------------------------------------------------------------- /telemetry_obd/obd_logger.py: -------------------------------------------------------------------------------- 1 | # telemetry_obd/obd_logger.py: Onboard Diagnostic Data Logger. 2 | """ 3 | telemetry_obd/obd_logger.py: Onboard Diagnostic Data Logger. 4 | """ 5 | from sys import stdout, stderr 6 | from os import fsync 7 | from time import sleep 8 | from datetime import datetime, timezone 9 | from pathlib import Path 10 | from argparse import ArgumentParser 11 | from pint import OffsetUnitCalculusError 12 | import sys 13 | import json 14 | import logging 15 | from traceback import print_exc 16 | import obd 17 | from .__init__ import __version__ 18 | 19 | from tcounter.common import ( 20 | get_config_file_path, 21 | get_output_file_name, 22 | get_next_application_counter_value, 23 | BASE_PATH, 24 | ) 25 | 26 | from .obd_common_functions import ( 27 | get_vin_from_vehicle, 28 | get_elm_info, 29 | CommandNameGenerator, 30 | clean_obd_query_response, 31 | get_obd_connection, 32 | recover_lost_connection, 33 | execute_obd_command, 34 | ) 35 | 36 | logger = logging.getLogger("obd_logger") 37 | 38 | FULL_CYCLES_COUNT = 5000 39 | TIMEOUT=1.0 # seconds 40 | DEFAULT_START_CYCLE_DELAY=0 # seconds 41 | 42 | def argument_parsing()-> dict: 43 | """Argument parsing""" 44 | parser = ArgumentParser(description="Telemetry OBD Logger") 45 | 46 | parser.add_argument( 47 | "base_path", 48 | nargs='?', 49 | metavar="base_path", 50 | default=[BASE_PATH, ], 51 | help=f"Relative or absolute output data directory. Defaults to '{BASE_PATH}'." 52 | ) 53 | 54 | parser.add_argument( 55 | "--config_file", 56 | help="Settings file name. Defaults to '.ini' or 'default.ini'.", 57 | default=None 58 | ) 59 | 60 | parser.add_argument( 61 | "--config_dir", 62 | help="Settings directory path. Defaults to './config'.", 63 | default='./config' 64 | ) 65 | 66 | parser.add_argument( 67 | '--full_cycles', 68 | type=int, 69 | default=FULL_CYCLES_COUNT, 70 | help=( 71 | "The number of full cycles before a new output file is started." + 72 | f" Default is {FULL_CYCLES_COUNT}." 73 | ) 74 | ) 75 | 76 | parser.add_argument( 77 | '--timeout', 78 | type=float, 79 | default=TIMEOUT, 80 | help=( 81 | "The number seconds before the current command times out." + 82 | f" Default is {TIMEOUT} seconds." 83 | ) 84 | ) 85 | 86 | parser.add_argument( 87 | "--logging", 88 | help="Turn on logging in python-obd library. Default is off.", 89 | default=False, 90 | action='store_true' 91 | ) 92 | 93 | parser.add_argument( 94 | "--no_fast", 95 | help="When on, commands for every request will be unaltered with potentially long timeouts " + 96 | "when the car doesn't respond promptly or at all. " + 97 | "When off (fast is on), commands are optimized before being sent to the car. A timeout is " + 98 | "added at the end of the command. Default is off. ", 99 | default=False, 100 | action='store_true' 101 | ) 102 | 103 | parser.add_argument( 104 | "--start_cycle_delay", 105 | help=f"Delay in seconds before first OBD command in cycle. Default is {DEFAULT_START_CYCLE_DELAY}.", 106 | default=DEFAULT_START_CYCLE_DELAY, 107 | type=float, 108 | ) 109 | 110 | parser.add_argument( 111 | "--verbose", 112 | help="Turn verbose output on. Default is off.", 113 | default=False, 114 | action='store_true' 115 | ) 116 | 117 | parser.add_argument( 118 | "--version", 119 | help="Print version number and exit.", 120 | default=False, 121 | action='store_true' 122 | ) 123 | 124 | return vars(parser.parse_args()) 125 | 126 | def main(): 127 | """ 128 | Run main function. 129 | """ 130 | 131 | args = argument_parsing() 132 | 133 | if args['version']: 134 | print(f"Version {__version__}", file=stdout) 135 | exit(0) 136 | 137 | fast = not args['no_fast'] 138 | timeout = args['timeout'] 139 | verbose = args['verbose'] 140 | debug = args['logging'] 141 | full_cycles = args['full_cycles'] 142 | start_cycle_delay = args['start_cycle_delay'] 143 | 144 | logging_level = logging.WARNING 145 | 146 | if verbose: 147 | logging_level = logging.INFO 148 | 149 | if debug: 150 | logging_level = logging.DEBUG 151 | 152 | logging.basicConfig(stream=sys.stdout, level=logging_level) 153 | obd.logger.setLevel(logging_level) 154 | 155 | logging.info(f"argument --fast: {fast}") 156 | logging.info(f"argument --timeout: {timeout}") 157 | logging.info(f"argument --verbose: {verbose}") 158 | logging.info(f"argument --full_cycles: {full_cycles}") 159 | logging.info(f"argument --logging: {args['logging']} ") 160 | logging.info(f"argument --start_cycle_delay: {start_cycle_delay}") 161 | logging.debug("debug logging enabled") 162 | 163 | # OBD(portstr=None, baudrate=None, protocol=None, fast=True, timeout=0.1, check_voltage=True) 164 | connection = get_obd_connection(fast=fast, timeout=timeout) 165 | 166 | elm_version, elm_voltage = get_elm_info(connection) 167 | logging.info(f"ELM VERSION: {elm_version} ELM VOLTAGE: {elm_voltage}") 168 | 169 | vin = get_vin_from_vehicle(connection) 170 | logging.info(f"VIN: {vin}") 171 | 172 | config_file = args['config_file'] 173 | config_dir = args['config_dir'] 174 | BASE_PATH = ''.join(args['base_path']) 175 | 176 | if config_file: 177 | config_path = Path(config_dir) / Path(config_file) 178 | else: 179 | config_path = get_config_file_path(vin) 180 | 181 | command_name_generator = CommandNameGenerator(config_path) 182 | 183 | first_command_name = command_name_generator.cycle_names[0] 184 | last_command_name = command_name_generator.cycle_names[-1] 185 | logging.info(f"first_command_name: {first_command_name}") 186 | logging.info(f"last_command_name: {last_command_name}") 187 | 188 | while command_name_generator: 189 | output_file_path = get_output_file_name('obd', vin=vin) 190 | logging.info(f"output file: {output_file_path}") 191 | 192 | try: 193 | # x - open for exclusive creation, failing if the file already exists 194 | with open(output_file_path, mode='x', encoding='utf-8') as out_file: 195 | 196 | for command_name in command_name_generator: 197 | if first_command_name == command_name: 198 | # insert delay here 199 | if start_cycle_delay > 0: 200 | sleep(start_cycle_delay) 201 | 202 | logging.info(f"command_name: {command_name}") 203 | 204 | if '-' in command_name: 205 | logging.error(f"skipping malformed command_name: {command_name}") 206 | continue 207 | 208 | iso_ts_pre = datetime.isoformat( 209 | datetime.now(tz=timezone.utc) 210 | ) 211 | 212 | try: 213 | 214 | obd_response = execute_obd_command(connection, command_name) 215 | 216 | except OffsetUnitCalculusError as e: 217 | logging.exception(f"Exception: {e.__class__.__name__}: {e}") 218 | logging.exception(f"OffsetUnitCalculusError on {command_name}, decoder must be fixed") 219 | print_exc() 220 | 221 | except Exception as e: 222 | logging.exception(f"Exception: {e}") 223 | print_exc() 224 | if not connection.is_connected(): 225 | logging.info(f"connection failure on {command_name}, reconnecting") 226 | connection.close() 227 | connection = get_obd_connection(fast=fast, timeout=timeout) 228 | 229 | iso_ts_post = datetime.isoformat( 230 | datetime.now(tz=timezone.utc) 231 | ) 232 | 233 | obd_response_value = clean_obd_query_response(command_name, obd_response) 234 | 235 | logging.info(f"saving: {command_name}, {obd_response_value}, {iso_ts_pre}, {iso_ts_post}") 236 | 237 | out_file.write(json.dumps({ 238 | 'command_name': command_name, 239 | 'obd_response_value': obd_response_value, 240 | 'iso_ts_pre': iso_ts_pre, 241 | 'iso_ts_post': iso_ts_post, 242 | }) + "\n" 243 | ) 244 | out_file.flush() 245 | fsync(out_file.fileno()) 246 | 247 | if not connection.is_connected(): 248 | logging.error(f"connection lost, retrying after {command_name}") 249 | connection = recover_lost_connection(connection, fast=fast, timeout=timeout) 250 | 251 | if ( 252 | command_name_generator.full_cycles_count > 253 | full_cycles 254 | ): 255 | command_name_generator.full_cycles_count = 0 256 | break 257 | 258 | except FileExistsError: 259 | logger.error(f"open(): FileExistsError: {output_file_path}") 260 | imu_counter = get_next_application_counter_value('obd') 261 | logger.error(f"get_log_file_handle(): Incremented 'obd' counter to {imu_counter}") 262 | 263 | if __name__ == "__main__": 264 | main() 265 | --------------------------------------------------------------------------------