├── .github └── workflows │ ├── python-publish.yml │ └── python-test.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG.rst ├── CONTRIBUTING.rst ├── CONTRIBUTORS ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── .gitignore ├── Makefile ├── _static │ ├── dashboard-raw.png │ ├── dashboard.png │ ├── logo.png │ └── rtd.css ├── api │ ├── api.rst │ ├── controllers.rst │ ├── formats.rst │ ├── measurements.rst │ ├── sinks.rst │ ├── sources.rst │ ├── units.rst │ ├── utils.rst │ └── vehicle.rst ├── conf.py ├── contributing.rst ├── example.rst ├── index.rst ├── installation.rst ├── pip-requirements.txt └── tools │ ├── codegen.rst │ ├── control.rst │ ├── dashboard.rst │ ├── diag.rst │ ├── dump.rst │ ├── gps.rst │ ├── obd2scanner.rst │ ├── scanner.rst │ ├── tools.rst │ └── tracesplit.rst ├── fabfile.py ├── openxc ├── __init__.py ├── controllers │ ├── __init__.py │ ├── base.py │ ├── serial.py │ └── usb.py ├── formats │ ├── __init__.py │ ├── base.py │ ├── binary.py │ └── json.py ├── generator │ ├── __init__.py │ ├── coder.py │ ├── message_sets.py │ ├── signals.cpp.footer │ ├── signals.cpp.header │ ├── structures.py │ └── xml_to_json.py ├── interface.py ├── logconfig.py ├── measurements.py ├── openxc_pb2.py ├── sinks │ ├── __init__.py │ ├── base.py │ ├── notifier.py │ ├── queued.py │ ├── recorder.py │ └── uploader.py ├── sources │ ├── __init__.py │ ├── base.py │ ├── bluetooth.py │ ├── network.py │ ├── serial.py │ ├── socket.py │ ├── trace.py │ └── usb.py ├── tools │ ├── __init__.py │ ├── args.py │ ├── common.py │ ├── control.py │ ├── dashboard.py │ ├── diagnostics.py │ ├── dump.py │ ├── generate_code.py │ ├── gps.py │ ├── obd2scanner.py │ ├── scanner.py │ ├── static │ │ ├── css │ │ │ └── dashboard.css │ │ └── js │ │ │ ├── dashboard.js │ │ │ ├── jquery-3.4.1.min.js │ │ │ ├── jquery.color-2.1.2.min.js │ │ │ └── socket.io.slim.js │ ├── templates │ │ └── dashboard.html │ └── tracesplit.py ├── units.py ├── utils.py ├── vehicle.py └── version.py ├── setup.py ├── tests ├── __init__.py ├── diagnostic.json.example ├── evented.json ├── handlers.cpp.example ├── mapped_signal_set.json.example ├── mapped_signals.json.example ├── signals.json.example ├── streamer_test_utils.py ├── test_binary_streamer.py ├── test_filerecorder.py ├── test_generate_diagnostic.py ├── test_generator.py ├── test_json_streamer.py ├── test_measurement.py ├── test_network.py ├── test_serial.py ├── test_trace.py ├── test_uploader.py ├── test_usb.py ├── test_vehicle.py ├── test_version.py ├── trace-no-timestamp.json ├── trace-shorter.json └── trace.json └── tox.ini /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.github/workflows/python-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Test Open XC Pyton 5 | 6 | on: [ pull_request ] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: [3.6.7] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | sudo apt-get update 25 | sudo apt-get install python-bluetooth -qq -y 26 | python -m pip install --upgrade pip 27 | python -m pip install pytest pyserial==3.1.1 coveralls 28 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 29 | 30 | - name: Test 31 | run: | 32 | python setup.py test 33 | 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.swo 4 | *.egg 5 | *.egg-info/ 6 | build/ 7 | dist/ 8 | MANIFEST 9 | .DS_Store 10 | .tox 11 | openxc-*/ 12 | *.json 13 | env 14 | *.gpx 15 | .coverage 16 | coverage.xml 17 | diffcover.html 18 | htmlcov 19 | *~ 20 | .scannerwork/ 21 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Chris Peplin 2 | Daniel Pronych 3 | David Melcher 4 | Zac Nelson 5 | Juergen Schmerder 6 | 7 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | OpenXC Python Library Changelog 2 | =============================== 3 | 4 | v2.2.0 5 | ---------- 6 | * Improvements to usb traffic handling 7 | * Updated message format based upton the protobuf file 8 | * Improved memory management for protobuf 9 | * SonarQube updates 10 | * Github actions updated,travis decommissioned 11 | * Support for the new get_vin command 12 | openxc-control get_vin 13 | 14 | v2.1.0 15 | ---------- 16 | * SonarQube integration 17 | * Fix: Modem configuration for c5 cellar build now properly sets baud rate 18 | * Fix: Protobuf general improvements 19 | * Keyboard interupt via c added to openxc-dump and Obd2 Scanner 20 | * Stitching Feature, large messages are now packaged and sent in smaller chunks from the vi 21 | 22 | v2.0.0 23 | ---------- 24 | * Known Issue: OpenXC python must be used with firmware 8.0.0 or greater. 25 | * Feature: openxc-generate-firmware-code generator now generates signals.cpp in a more memory efficent way. 26 | * Feature: Add dashboard on message change highlighting. 27 | * Feature: Add column sorting and filtering on dashboard. 28 | * Feature: Add more verbosity to libusb errors. 29 | * Fix: Fix more python 3 migration byte bugs. 30 | 31 | v1.1.1 32 | ---------- 33 | * Fix: Fixed Pip included files 34 | 35 | v1.1.0 36 | ---------- 37 | * Feature: Generate firmware now checks for duplicate entries in json and for improperly used keys and unrecognised attributes. 38 | * Feature: openxc-dashboad has been updated from a command line tool to a web server and webpage. 39 | * Fix: Requiring windows curses on non windows system 40 | * Fix: Various Python 3 bugs 41 | 42 | v1.0.0 43 | ---------- 44 | * Remove python 2.7 support 45 | * Add python 3.6 support 46 | * Known Issue: Tool openxc-dashboard does not display correctly in windows 47 | 48 | v0.15.0 49 | ---------- 50 | 51 | * Fix: Install curses on windows 52 | * Fix: Multiple dependencies updated due to vulnerabilities 53 | 54 | v0.14.0 55 | ---------- 56 | 57 | * Fix: Remove support for Python 2.6 58 | * Fix: Diagnostic response cleanup 59 | * Feature: Add "platform" command support 60 | * Fix: Documentation improvements 61 | * Fix: openxc-dump command file access 62 | 63 | v0.13.0 64 | ---------- 65 | 66 | * Feature: Support for new C5 Cellular, SD, and RTC commands 67 | 68 | v0.12.0 69 | ----------- 70 | 71 | * Feature: Support scanning for Bluetooth devices when using Linux 72 | * Feature: Support connecting to Bluetooth VIs directly from the library in 73 | Linux using BlueZ. 74 | * Feature: Support adding and cancelling diagnostic requests. 75 | * Feature: Read 'status' field in command responses. 76 | * Feature: Allow explicitly setting the payload format when sending and 77 | receiving commands with a VI. 78 | * Feature: Support command to control status of CAN acceptance filter bypass. 79 | * Feature: Support controlling passthrough status of CAN buses in VI. 80 | * Feature: Support 'loopback' attribute of CAN buses in firmware config. 81 | * Feature: Support setting desired decoding type for diagnostic requests. 82 | * Feature: Support command to change payload format of VI. 83 | * Improvement: Better balance between big efficient reads and quick signal 84 | responses when receiving via USB. 85 | * Improvement: Remove now unnecessary sleep from command line tools. 86 | * Improvement: Complete full support for protobuf message deserialization. 87 | * Improvement: Improve unit test coverage from 27% to 58%. 88 | * Fix: Use correct abbreviation for kilometer unit. 89 | * Fix: Remove deprecated signal attribute from example code. 90 | * Fix: Spool up message receivers before sending requests to make sure not to 91 | miss responses. 92 | 93 | v0.11.3 94 | ---------- 95 | 96 | * Remove embedded platform-specific code from generated signals file for 97 | firmware (to match vi-firmware v6.0.2). 98 | * Add documentation on installing in Windows without Cygwin. 99 | 100 | v0.11.2 101 | ---------- 102 | 103 | * Fixed parsing of v6.x style OpenXC messages from a network data source. 104 | * Fixed use of const variables in generated code. 105 | * Fixed a code gen. regression where custom C++ functions were called before 106 | being declared / defined. 107 | * Include OBD2 message handler in generated code so it can be used as a handler 108 | without custom source code files. 109 | * Don't require a working network connection and DNS to run the test suite. 110 | 111 | v0.11.1 112 | ---------- 113 | 114 | * Fixed receiving data from new network based VI using null delimiters 115 | * Make sure const variables are initialized in generated firmware code. 116 | 117 | v0.11 118 | ---------- 119 | 120 | * Support communicating with vi-firmware v6.x. 121 | * Support generating code for vi-firmware v6.x. 122 | * Support control commands over serial in additional to USB (version, device ID 123 | diagnostics, raw CAN messages). 124 | * Read debug log data over USB from a VI (see the `log-mode` flag on the CLI 125 | tools). 126 | * Support diagnostic request/response exchange with a v6.x VI. 127 | * Add `openxc-diag`, `openxc-scanner` and `openxc-obd2scanner` CLI tools. 128 | * Increase Bluetooth VI connection reliabilty when sending commands. 129 | 130 | v0.10.3 131 | ---------- 132 | 133 | * In generated code, include the extra sources after messages and signals so 134 | they are already defined. 135 | 136 | v0.10.2 137 | ---------- 138 | 139 | * Fix bit_numbering_inverted flag override in code generation. 140 | * Fix using only 1 CAN bus with code generation. 141 | 142 | v0.10.1 143 | ---------- 144 | 145 | * Rename and supported 'raw_writable' flag on CAN bus in VI configuration. 146 | 147 | v0.10 148 | ---------- 149 | 150 | * Dropped support for Python 3 - needed to add protobuf dependency, which 151 | doesn't work with Python 3 yet. 152 | * Significant speedup in VI firmware code generation with simple parsed XML 153 | caching 154 | * Parse binary output payload from a vehicle interface (experimental) 155 | * Small bug fixes and efficiency improvements with code generation. 156 | 157 | v0.9.5 158 | ---------- 159 | 160 | * Improve screen width detection in openxc-dashboard 161 | * Add veritcal keyboard scrolling to openxc-dashboard 162 | * Support displaying raw CAN messages in openxc-dashboard 163 | * Allow registering a listener for all measurements 164 | * Fix non-looping trace file playback 165 | * Allow playing back raw CAN trace files. 166 | * Updated to work with v5.x of VI firmware. 167 | * Allow a message to have multiple handlers 168 | * Fix a bug that disallowed ignoring a signal with states defined 169 | * Add max_message_frequency and force_send_changed_signals to messages 170 | * Add max_frequency and force_send_changed to signals 171 | * Allow overriding bit inversion on a per-signal basis 172 | * Define as many things const as possible to save memory in VI 173 | * Add 'unfiltered' and 'filtered' raw CAN modes 174 | * Allow setting max CAN message freq for all buses. 175 | * Default to inverted bit mapping only if using a database-backed mapping. 176 | 177 | v0.9.4 178 | ---------- 179 | 180 | * Move vehicle interface code generation utilites from cantranslator repository 181 | to this Python library 182 | * Support playing back a raw CAN trace file at the same speed it was recorded. 183 | 184 | v0.9.3 185 | ------ 186 | 187 | * Fix openxc-dashboard in Python 3.3. 188 | * Increase robustness to invalid messages. 189 | * Update pre-programmed OpenXC signals to match v4.0 release of the OpenXC 190 | vehicle interface (VI). 191 | * Match defaut serial baud rate to v4.0 release of OpenXC VI 192 | * Other small bug fixes and improvements. 193 | 194 | v0.9.2 195 | ------ 196 | 197 | Botched this release. 198 | 199 | v0.9.1 200 | ------ 201 | 202 | * Make pyserial an optional dependency to work around an issue with that package 203 | in Python 3 204 | 205 | v0.9 206 | ---- 207 | 208 | * Initial release 209 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | How to contribute 3 | ==================== 4 | 5 | Contributions are welcome - we want to keep it as easy as possible to contribute 6 | changes that get things working in your environment. There are a few guidelines 7 | that we need contributors to follow so that we can have a chance of keeping on 8 | top of things. 9 | 10 | Reporting an Issue 11 | ================== 12 | 13 | * Make sure you have a `GitHub account `_ 14 | * Check if a ticket already exists for your issue on GitHub_. 15 | * If one does not already exist, `create a new ticket`__ 16 | + Clearly describe the issue including steps to reproduce when it is a bug. 17 | + Make sure you include in the earliest version that you know has the issue. 18 | 19 | __ GitHub_ 20 | 21 | Making Changes 22 | ================== 23 | 24 | * If you haven't already, create a new issue on GitHub_ for your bug 25 | fix or new feature. 26 | * Fork the repository on GitHub 27 | * Create a topic branch from where you want to base your work. 28 | + This is usually the master branch. 29 | + Only target release branches if you are certain your fix must be on that branch. 30 | + To quickly create a topic branch based on master: 31 | ``git checkout -b fix/master/my_contribution master`` 32 | + Please avoid working directly on the ``master`` branch. 33 | * Make commits of logical units. 34 | * Check for unnecessary whitespace with `git diff --check` before committing. 35 | * Make sure your commit messages are in the `proper 36 | format `_. 37 | * Make sure you have added the necessary tests for your changes 38 | * Run the `full test 39 | suite `_ 40 | to assure nothing else was accidentally broken. 41 | 42 | Submitting Changes 43 | ================== 44 | 45 | * Push your changes to a topic branch in your fork of the repository. 46 | * Sign the `Contributor License Agreement 47 | `_ (CLA) 48 | * Submit a pull request to the repository in the openxc organization. 49 | 50 | Additional Resources 51 | ================== 52 | 53 | * `General GitHub documentation`_ 54 | * GitHub `pull request documentation`_ 55 | 56 | .. _`General GitHub Documentation`: http://help.github.com/ 57 | .. _`pull request documentation`: http://help.github.com/send-pull-requests/ 58 | .. _GitHub: https://github.com/openxc/openxc-python/issues 59 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openxc/openxc-python/5341180fea6c364027dedc9bc4c8027b2831325f/CONTRIBUTORS -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Ford Motor Company 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include CONTRIBUTING.rst 3 | include LICENSE 4 | include openxc/generator/signals.cpp.footer 5 | include openxc/generator/signals.cpp.header 6 | include openxc/tools/templates/dashboard.html 7 | include openxc/tools/static/css/dashboard.css 8 | include openxc/tools/static/js/jquery-3.4.1.min.js 9 | include openxc/tools/static/js/dashboard.js 10 | include openxc/tools/static/js/socket.io.slim.js 11 | include openxc/tools/static/js/jquery.color-2.1.2.min.js 12 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============================================== 2 | OpenXC for Python 3 | =============================================== 4 | 5 | .. image:: https://github.com/openxc/openxc-python/raw/master/docs/_static/logo.png 6 | 7 | :Version: 2.2.0 8 | :Web: http://openxcplatform.com 9 | :Download: http://pypi.python.org/pypi/openxc/ 10 | :Documentation: http://python.openxcplatform.com 11 | :Source: http://github.com/openxc/openxc-python/ 12 | :Keywords: vehicle, openxc, python 13 | 14 | .. image:: https://github.com/openxc/openxc-python/workflows/Test%20Open%20XC%20Pyton/badge.svg 15 | :target: https://github.com/openxc/openxc-python/actions?query=workflow%3A%22Test+Open+XC+Pyton%22 16 | 17 | .. image:: https://coveralls.io/repos/openxc/openxc-python/badge.png?branch=master 18 | :target: https://coveralls.io/r/openxc/openxc-python?branch=master 19 | 20 | .. image:: https://readthedocs.org/projects/openxc-python-library/badge/ 21 | :target: http://python.openxcplatform.com 22 | :alt: Documentation Status 23 | 24 | The OpenXC Python library (for Python 3.6.7) provides an interface to 25 | vehicle data from the OpenXC Platform. The primary platform for OpenXC 26 | applications is Android, but for prototyping and testing, often it is 27 | preferrable to use a low-overhead environment like Python when developing. 28 | 29 | In addition to a port of the Android library API, the package also contains a 30 | number of command-line tools for connecting to the CAN translator and 31 | manipulating previously recorded vehicle data. 32 | 33 | If you are getting the error "ValueError: No backend available" on windows please reinstall your libusb0 driver using https://github.com/openxc/vi-windows-driver if you are in a envirment where you can not use an unsigned driver please use https://zadig.akeo.ie/ 34 | 35 | Due to changes in signals.cpp openxc-python Version 2.0.0 must be used with vi-firmware 8.0.0 or greater. 36 | Due to changes with large diagnostic responses Version 2.1.0 must be used with vi-firmware 8.1.0 or greater. 37 | 38 | To package run "setup.py sdist bdist_wheel" 39 | to push to pypi run "python -m twine upload dist/\*" 40 | Version files: 41 | 42 | - CHANGELOG.rst 43 | - README.rst 44 | - openxc/version.py 45 | - docs/index.rst 46 | 47 | License 48 | ======== 49 | 50 | Copyright (c) 2012-2020 Ford Motor Company 51 | 52 | Licensed under the BSD license. 53 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/OpenXCforPython.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/OpenXCforPython.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/OpenXCforPython" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/OpenXCforPython" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/_static/dashboard-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openxc/openxc-python/5341180fea6c364027dedc9bc4c8027b2831325f/docs/_static/dashboard-raw.png -------------------------------------------------------------------------------- /docs/_static/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openxc/openxc-python/5341180fea6c364027dedc9bc4c8027b2831325f/docs/_static/dashboard.png -------------------------------------------------------------------------------- /docs/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openxc/openxc-python/5341180fea6c364027dedc9bc4c8027b2831325f/docs/_static/logo.png -------------------------------------------------------------------------------- /docs/api/api.rst: -------------------------------------------------------------------------------- 1 | Vehicle Data API Reference 2 | ========================== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | :glob: 7 | 8 | * 9 | -------------------------------------------------------------------------------- /docs/api/controllers.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Controllers 3 | =========== 4 | 5 | .. automodule:: openxc.controllers.base 6 | :members: 7 | :undoc-members: 8 | 9 | .. automodule:: openxc.controllers.serial 10 | :members: 11 | :undoc-members: 12 | 13 | .. automodule:: openxc.controllers.usb 14 | :members: 15 | :undoc-members: 16 | -------------------------------------------------------------------------------- /docs/api/formats.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Data Formats 3 | ============ 4 | 5 | .. automodule:: openxc.formats.json 6 | :members: 7 | :undoc-members: 8 | -------------------------------------------------------------------------------- /docs/api/measurements.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Measurements 3 | ============ 4 | 5 | .. automodule:: openxc.measurements 6 | :members: 7 | :undoc-members: 8 | -------------------------------------------------------------------------------- /docs/api/sinks.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Data Sinks 3 | ========== 4 | 5 | .. automodule:: openxc.sinks.base 6 | :members: 7 | :undoc-members: 8 | 9 | .. automodule:: openxc.sinks.notifier 10 | :members: 11 | :undoc-members: 12 | 13 | .. automodule:: openxc.sinks.queued 14 | :members: 15 | :undoc-members: 16 | 17 | .. automodule:: openxc.sinks.recorder 18 | :members: 19 | :undoc-members: 20 | 21 | .. automodule:: openxc.sinks.uploader 22 | :members: 23 | :undoc-members: 24 | -------------------------------------------------------------------------------- /docs/api/sources.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Data Sources 3 | ============ 4 | 5 | .. automodule:: openxc.sources.base 6 | :members: 7 | :undoc-members: 8 | 9 | .. automodule:: openxc.sources.serial 10 | :members: 11 | :undoc-members: 12 | 13 | .. automodule:: openxc.sources.usb 14 | :members: 15 | :undoc-members: 16 | 17 | .. automodule:: openxc.sources.trace 18 | :members: 19 | :undoc-members: 20 | 21 | .. automodule:: openxc.sources.network 22 | :members: 23 | :undoc-members: 24 | -------------------------------------------------------------------------------- /docs/api/units.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Units 3 | ========== 4 | 5 | .. automodule:: openxc.units 6 | 7 | .. autoclass:: Percentage 8 | .. autoclass:: Meter 9 | .. autoclass:: Kilometer 10 | .. autoclass:: Hour 11 | .. autoclass:: KilometersPerHour 12 | .. autoclass:: RotationsPerMinute 13 | .. autoclass:: Litre 14 | .. autoclass:: Degree 15 | .. autoclass:: NewtonMeter 16 | .. autoclass:: MetersPerSecondSquared 17 | .. autoclass:: Undefined 18 | -------------------------------------------------------------------------------- /docs/api/utils.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Utils 3 | ========== 4 | 5 | .. automodule:: openxc.utils 6 | :members: 7 | :undoc-members: 8 | -------------------------------------------------------------------------------- /docs/api/vehicle.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Vehicle functions 3 | ====================== 4 | 5 | .. automodule:: openxc.vehicle 6 | :members: 7 | :undoc-members: 8 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # OpenXC for Python documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Oct 4 10:54:31 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = 'OpenXC for Python' 44 | copyright = '2017, Ford Motor Company' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | 51 | sys.path.insert(0, os.path.abspath(os.path.join(os.getcwd(), '..'))) 52 | from openxc.version import get_version 53 | 54 | # The short X.Y version. 55 | version = get_version() 56 | # The full version, including alpha/beta/rc tags. 57 | release = get_version() 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | #language = None 62 | 63 | # There are two options for replacing |today|: either, you set today to some 64 | # non-false value, then it is used: 65 | #today = '' 66 | # Else, today_fmt is used as the format for a strftime call. 67 | #today_fmt = '%B %d, %Y' 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | exclude_patterns = ['_build'] 72 | 73 | # The reST default role (used for this markup: `text`) to use for all documents. 74 | #default_role = None 75 | 76 | # If true, '()' will be appended to :func: etc. cross-reference text. 77 | #add_function_parentheses = True 78 | 79 | # If true, the current module name will be prepended to all description 80 | # unit titles (such as .. function::). 81 | #add_module_names = True 82 | 83 | # If true, sectionauthor and moduleauthor directives will be shown in the 84 | # output. They are ignored by default. 85 | #show_authors = False 86 | 87 | # The name of the Pygments (syntax highlighting) style to use. 88 | pygments_style = 'sphinx' 89 | 90 | # A list of ignored prefixes for module index sorting. 91 | #modindex_common_prefix = [] 92 | 93 | 94 | # -- Options for HTML output --------------------------------------------------- 95 | 96 | # The theme to use for HTML and HTML Help pages. See the documentation for 97 | # a list of builtin themes. 98 | 99 | try: 100 | import sphinx_rtd_theme 101 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 102 | except ImportError: 103 | pass 104 | 105 | html_theme = "sphinx_rtd_theme" 106 | 107 | RTD_NEW_THEME = True 108 | 109 | # Theme options are theme-specific and customize the look and feel of a theme 110 | # further. For a list of options available for each theme, see the 111 | # documentation. 112 | #html_theme_options = {} 113 | 114 | # Add any paths that contain custom themes here, relative to this directory. 115 | #html_theme_path = [] 116 | 117 | # The name for this set of Sphinx documents. If None, it defaults to 118 | # " v documentation". 119 | #html_title = None 120 | 121 | # A shorter title for the navigation bar. Default is the same as html_title. 122 | #html_short_title = None 123 | 124 | # The name of an image file (relative to this directory) to place at the top 125 | # of the sidebar. 126 | #html_logo = None 127 | 128 | # The name of an image file (within the static path) to use as favicon of the 129 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 130 | # pixels large. 131 | #html_favicon = None 132 | 133 | # Add any paths that contain custom static files (such as style sheets) here, 134 | # relative to this directory. They are copied after the builtin static files, 135 | # so a file named "default.css" will overwrite the builtin "default.css". 136 | html_static_path = ['_static'] 137 | 138 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 139 | # using the given strftime format. 140 | #html_last_updated_fmt = '%b %d, %Y' 141 | 142 | # If true, SmartyPants will be used to convert quotes and dashes to 143 | # typographically correct entities. 144 | #html_use_smartypants = True 145 | 146 | # Custom sidebar templates, maps document names to template names. 147 | #html_sidebars = {} 148 | 149 | # Additional templates that should be rendered to pages, maps page names to 150 | # template names. 151 | #html_additional_pages = {} 152 | 153 | # If false, no module index is generated. 154 | #html_domain_indices = True 155 | 156 | # If false, no index is generated. 157 | #html_use_index = True 158 | 159 | # If true, the index is split into individual pages for each letter. 160 | #html_split_index = False 161 | 162 | # If true, links to the reST sources are added to the pages. 163 | #html_show_sourcelink = True 164 | 165 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 166 | #html_show_sphinx = True 167 | 168 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 169 | #html_show_copyright = True 170 | 171 | # If true, an OpenSearch description file will be output, and all pages will 172 | # contain a tag referring to it. The value of this option must be the 173 | # base URL from which the finished HTML is served. 174 | #html_use_opensearch = '' 175 | 176 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 177 | #html_file_suffix = None 178 | 179 | # Output file base name for HTML help builder. 180 | htmlhelp_basename = 'OpenXCforPythondoc' 181 | 182 | 183 | # -- Options for LaTeX output -------------------------------------------------- 184 | 185 | latex_elements = { 186 | # The paper size ('letterpaper' or 'a4paper'). 187 | #'papersize': 'letterpaper', 188 | 189 | # The font size ('10pt', '11pt' or '12pt'). 190 | #'pointsize': '10pt', 191 | 192 | # Additional stuff for the LaTeX preamble. 193 | #'preamble': '', 194 | } 195 | 196 | # Grouping the document tree into LaTeX files. List of tuples 197 | # (source start file, target name, title, author, documentclass [howto/manual]). 198 | latex_documents = [ 199 | ('index', 'OpenXCforPython.tex', 'OpenXC for Python Documentation', 200 | 'Christopher Peplin', 'manual'), 201 | ] 202 | 203 | # The name of an image file (relative to this directory) to place at the top of 204 | # the title page. 205 | #latex_logo = None 206 | 207 | # For "manual" documents, if this is true, then toplevel headings are parts, 208 | # not chapters. 209 | #latex_use_parts = False 210 | 211 | # If true, show page references after internal links. 212 | #latex_show_pagerefs = False 213 | 214 | # If true, show URL addresses after external links. 215 | #latex_show_urls = False 216 | 217 | # Documents to append as an appendix to all manuals. 218 | #latex_appendices = [] 219 | 220 | # If false, no module index is generated. 221 | #latex_domain_indices = True 222 | 223 | 224 | # -- Options for manual page output -------------------------------------------- 225 | 226 | # One entry per manual page. List of tuples 227 | # (source start file, name, description, authors, manual section). 228 | man_pages = [ 229 | ('index', 'openxcforpython', 'OpenXC for Python Documentation', 230 | ['Christopher Peplin'], 1) 231 | ] 232 | 233 | # If true, show URL addresses after external links. 234 | #man_show_urls = False 235 | 236 | 237 | # -- Options for Texinfo output ------------------------------------------------ 238 | 239 | # Grouping the document tree into Texinfo files. List of tuples 240 | # (source start file, target name, title, author, 241 | # dir menu entry, description, category) 242 | texinfo_documents = [ 243 | ('index', 'OpenXCforPython', 'OpenXC for Python Documentation', 244 | 'Christopher Peplin', 'OpenXCforPython', 'One line description of project.', 245 | 'Miscellaneous'), 246 | ] 247 | 248 | # Documents to append as an appendix to all manuals. 249 | #texinfo_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | #texinfo_domain_indices = True 253 | 254 | autoclass_content = 'both' 255 | 256 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 257 | #texinfo_show_urls = 'footnote' 258 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Development of ``openxc-python`` happens at `GitHub`_. Be sure to see our `contribution document`_ for details. 5 | 6 | .. _`GitHub`: https://github.com/openxc/openxc-python 7 | .. _`contribution document`: https://github.com/openxc/openxc-python/blob/master/CONTRIBUTING.rst 8 | 9 | Test Suite 10 | ---------- 11 | 12 | The ``openxc-python`` repository contains a test suite that can be run with the 13 | ``tox`` tool, which attemps to run the test suite in Python 2.7. If 14 | you wish to just run the test suite in your primary Python version, run 15 | 16 | .. code-block:: sh 17 | 18 | $ python setup.py test 19 | 20 | To run it with tox: 21 | 22 | .. code-block:: sh 23 | 24 | $ tox 25 | 26 | Mailing list 27 | ------------ 28 | 29 | For discussions about the usage, development, and future of OpenXC, please join 30 | the `OpenXC mailing list`_. 31 | 32 | .. _`OpenXC mailing list`: http://groups.google.com/group/openxc 33 | 34 | Bug tracker 35 | ------------ 36 | 37 | If you have any suggestions, bug reports or annoyances please report them 38 | to our issue tracker at http://github.com/openxc/openxc-python/issues/ 39 | 40 | Authors and Contributors 41 | ------------------------ 42 | 43 | A `complete list 44 | `_ of all 45 | authors is stored in the repository - thanks to everyone for the great 46 | contributions. 47 | -------------------------------------------------------------------------------- /docs/example.rst: -------------------------------------------------------------------------------- 1 | Example Code 2 | ============= 3 | 4 | Read an unfiltered stream of OpenXC messages from a USB vehicle interface: 5 | 6 | .. code-block:: python 7 | 8 | from openxc.interface import UsbVehicleInterface 9 | 10 | def receive(message, **kwargs): 11 | # this callback will receive each message received as a dict 12 | print(message['name']) 13 | 14 | vi = UsbVehicleInterface(callback=receive) 15 | vi.start() 16 | # This will block until the connection dies or you exit the program 17 | vi.join() 18 | 19 | If you want to connect to a Bluetooth interface (currently only supported in 20 | Linux), just replace ``UsbVehicleInterface`` with ``BluetoothVehicleInterface``. 21 | 22 | The base ``VehicleInterface`` classes all implement the ``Controller`` API, 23 | which also supports writing CAN messages, creating diagnostic requests and 24 | sending configuration commands to the VI. 25 | 26 | For example, to create a diagnostic request and wait for responses: 27 | 28 | .. code-block:: python 29 | 30 | message_id = 42 31 | mode = 1 32 | bus = 1 33 | pid = 3 34 | 35 | responses = vi.create_diagnostic_request(message, mode, 36 | bus=bus, pid=pid, wait_for_first_response=True) 37 | 38 | To write a low-level CAN message (the VI must be configured to allow this): 39 | 40 | .. code-block:: python 41 | 42 | vi.write(bus=1, id=42, data="0x1234567812345678") 43 | 44 | To put the CAN acceptance filter in bypass mode for bus 1: 45 | 46 | .. code-block:: python 47 | 48 | vi.set_passthrough(1, true) 49 | 50 | There are many more commands and options, and most have documented APIs in the 51 | code base. You are encouraged you to dig around as the library is fairly small 52 | and should be easy to grok. More examples and documentation would be a most 53 | welcome contribution! 54 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | OpenXC for Python 3 | =================== 4 | 5 | .. image:: https://github.com/openxc/openxc-python/raw/master/docs/_static/logo.png 6 | 7 | :Version: 2.2.0 8 | :Web: http://openxcplatform.com 9 | :Download: http://pypi.python.org/pypi/openxc/ 10 | :Documentation: http://python.openxcplatform.com 11 | :Source: http://github.com/openxc/openxc-python/ 12 | 13 | The OpenXC Python library (for Python 3.6) provides an interface to 14 | vehicle data from the OpenXC Platform. The primary platform for OpenXC 15 | applications is Android, but for prototyping and testing, often it is 16 | preferrable to use a low-overhead environment like Python when developing. 17 | 18 | In addition to a port of the Android library API, the package also contains a 19 | number of command-line tools for connecting to the vehicle interface and 20 | manipulating previously recorded vehicle data. 21 | 22 | This Python package works with Python 3.6. For python 2.7 support use version 0.15.0. 23 | 24 | For general documentation on the OpenXC platform, visit the main `OpenXC site`_. 25 | 26 | .. _`OpenXC site`: http://openxcplatform.com 27 | 28 | .. toctree:: 29 | :maxdepth: 1 30 | 31 | installation 32 | tools/tools 33 | example 34 | api/api 35 | contributing 36 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Install Python and Pip 5 | ---------------------- 6 | 7 | This library (obviously) requires a Python language runtime - the OpenXC library 8 | supports python 3.x only, for python 2.x support use version 0.15.0. 9 | 10 | - **Mac OS X and Linux** 11 | 12 | Mac OS X and most Linux distributions already have a compatible Python 13 | installed. Run ``python --version`` from a terminal to check - you need a 14 | 3.6.x version, such as 3.6.8. 15 | 16 | - **Windows** 17 | 18 | #. Download and run the `Python 3.6.x MSI installer `_. 19 | Make sure to select to option to ``Add python.exe to Path``. 20 | #. Add the Python Scripts directory your PATH: 21 | ``PATH=%PATH%;C:\Users\%username%\AppData\Local\Programs\Python\Python36-32\Scripts``. If you aren't sure how to edit your 22 | ``PATH``, see `this guide for all versions of Windows 23 | `_. Log out and back in for 24 | the change to take effect. 25 | #. Install `pip `_, a 26 | Python package manager by saving the ``get-pip.py`` script to a file and 27 | running it from a terminal. 28 | 29 | - **Cygwin** 30 | 31 | From the ``setup.exe`` package list, select the ``python`` and 32 | ``python-setuptools`` packages. Then, inside Cygwin install ``pip`` using 33 | ``easy_install``: 34 | 35 | .. code-block:: sh 36 | 37 | $ easy_install pip 38 | 39 | Install the openxc Package 40 | -------------------------- 41 | 42 | You can install or upgrade the OpenXC library from the Python Package Index (PyPI) with 43 | ``pip`` at the command line: 44 | 45 | .. code-block:: sh 46 | 47 | $ [sudo] pip install -U openxc 48 | 49 | .. _usb: 50 | 51 | USB Backend 52 | ------------- 53 | 54 | If you intend to use the library to connect to a vehicle interface via USB, you 55 | must also install a native USB backend - ``libusb-1.0`` is the recommended 56 | library. 57 | 58 | - **Mac OS X** 59 | 60 | First install Homebrew_, then run:: 61 | 62 | $ brew install libusb 63 | 64 | .. _Homebrew: http://mxcl.github.com/homebrew/ 65 | 66 | - **Ubuntu** 67 | 68 | ``libusb`` is available in the main repository:: 69 | 70 | $ sudo apt-get install libusb-1.0-0 71 | 72 | - **Arch Linux** 73 | 74 | Install ``libusb`` using ``pacman``:: 75 | 76 | $ sudo pacman -S libusbx 77 | 78 | - **Windows** 79 | 80 | Download and install the `OpenXC VI USB driver`_. You must install the 81 | driver manually through the Device Manager while the VI is plugged in and 82 | on - either running the emulator firmware so it never turns off, or plugged 83 | into a real car. 84 | 85 | - **Cygwin** 86 | 87 | Install the VI USB driver as in a regular Windows installation. 88 | 89 | If you get the error ``Skipping USB device: [Errno 88] Operation not 90 | supported or unimplemented on this platform`` when you run any of the OpenXC 91 | Python tools, make sure you **do not** have the ``libusb`` Cygwin package 92 | installed - that is explicitly not compatible. 93 | 94 | .. _`OpenXC VI USB driver`: https://github.com/openxc/vi-windows-driver 95 | 96 | Using the development version 97 | ----------------------------- 98 | 99 | You can clone the repository and install the development version like so: 100 | 101 | .. code-block:: sh 102 | 103 | $ git clone https://github.com/openxc/openxc-python 104 | $ cd openxc-python 105 | $ pip install -e . 106 | 107 | Any time you update the clone of the Git repository, all of the Python tools 108 | will be updated too. 109 | 110 | -------------------------------------------------------------------------------- /docs/pip-requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx_rtd_theme 3 | -------------------------------------------------------------------------------- /docs/tools/codegen.rst: -------------------------------------------------------------------------------- 1 | ============================================= 2 | ``openxc-generate-firmware-code`` - configure CAN messages, signals and buses 3 | ============================================= 4 | 5 | The `OpenXC vehicle interface 6 | firmware `_ uses a JSON-formatted 7 | configuration file to set up CAN messages, signals and buses. The configuration 8 | options and many examples are included with the `VI firmware docs 9 | `_. The 10 | configuration file is used to generate C++ that is compiled with the open source 11 | firmware. 12 | 13 | The OpenXC Python library contains a command line tool, 14 | ``openxc-generate-firmware-code``, that can parse VI configuration files and 15 | generate a proper C++ implementation to compile the VI firmware. 16 | 17 | Once you've created a VI configuration file, run the 18 | ``openxc-generate-firmware-code`` tool to create an implementation of 19 | the functions in the VI's ``signals.h``. In this example, the configuration is 20 | in the file ``mycar.json``. 21 | 22 | .. code-block:: sh 23 | 24 | $ openxc-generate-firmware-code --message-set mycar.json > signals.cpp 25 | -------------------------------------------------------------------------------- /docs/tools/control.rst: -------------------------------------------------------------------------------- 1 | ===================================================================== 2 | ``openxc-control`` - write messages to the VI and change its settings 3 | ===================================================================== 4 | 5 | :program:`openxc-control` is a command-line tool that can send control messages 6 | to an attached vehicle interface. 7 | 8 | Basic use 9 | ========= 10 | 11 | -------- 12 | version 13 | -------- 14 | 15 | Print the current firmware version and firmware description of the attached CAN 16 | translator: 17 | 18 | .. code-block:: bash 19 | 20 | $ openxc-control version 21 | 22 | An example response is ``v7.2.1 (default)``, where the ``default`` firmware 23 | description can be modified in the JSON file via the ``"name"`` entry. 24 | The first example in `Low-level CAN Features `_ 25 | would return a value of ``passthrough``. 26 | 27 | --- 28 | id 29 | --- 30 | 31 | Print the unique ID of the VI, if it has one. This is often the MAC address of 32 | the Bluetooth module. 33 | 34 | .. code-block:: bash 35 | 36 | $ openxc-control id 37 | 38 | --------- 39 | platform 40 | --------- 41 | 42 | Print the hardware platform of the vehicle interface. 43 | 44 | .. code-block:: bash 45 | 46 | $ openxc-control platform 47 | 48 | Supported platforms include ``CHIPKIT``, ``BLUEBOARD``, ``FORDBOARD``, ``CROSSCHASM_C5``, ``CROSSCHASM_C5_BT``, ``CROSSCHASM_C5_BLE``, and ``CROSSCHASM_C5_CELLULAR`` 49 | 50 | --- 51 | set 52 | --- 53 | 54 | Modify the run-time configuration of the VI. Currently, you can change the 55 | acceptance filter (AF) bypass status, passthrough CAN message output, and the 56 | payload format used from the OpenXC message format. 57 | 58 | Enable and disable CAN AF bypass for a bus: 59 | 60 | .. code-block:: bash 61 | 62 | $ openxc-control set --bus 1 --af-bypass 63 | $ openxc-control set --bus 1 --no-af-bypass 64 | 65 | Enable and disable passthrough of CAN messages to the output interface (e.g. USB 66 | or Bluetooth): 67 | 68 | .. code-block:: bash 69 | 70 | $ openxc-control set --bus 1 --passthrough 71 | $ openxc-control set --bus 1 --no-passthrough 72 | 73 | Change the payload format to Protocol Buffers, then back to JSON: 74 | 75 | .. code-block:: bash 76 | 77 | $ openxc-control set --new-payload-format json 78 | $ openxc-control set --new-payload-format protobuf 79 | 80 | Change the time for the RTC unit on the C5 devices: 81 | 82 | .. code-block:: bash 83 | 84 | $ openxc-control set --time 1461545558 85 | 86 | Set the host and port for the C5 Cellular device 87 | 88 | .. code-block:: bash 89 | 90 | $ openxc-control set --host www.server.com --port 80 91 | 92 | This will return true when successful. If network-host is supplied, but not port, 93 | port will default to 80. 94 | 95 | ------ 96 | write 97 | ------ 98 | 99 | Send a write request to the VI, either for a simple vehicle message write (to be 100 | translated by the VI to a CAN message), or a raw CAN message. 101 | 102 | To write a simple vehicle message, the ``--name`` and ``--value`` parameters are 103 | required. The ``--event`` parameter is optional. 104 | 105 | .. code-block:: bash 106 | 107 | $ openxc-control write --name turn_signal_status --value left 108 | 109 | To write a CAN messages, the ``--bus``, ``--id`` and ``--data`` parameters are 110 | required. ``data`` should be a hex string. 111 | 112 | .. code-block:: bash 113 | 114 | $ openxc-control write --bus 1 --id 0x124 --data 0x0234567812345678 115 | 116 | A CAN message with an ID greater than can be represented with 11 bits 117 | will automatically be sent using the extended frame format. If you want to send 118 | a message with a lower ID using the extended frame format, you can use the 119 | ``--frame-format`` flag: 120 | 121 | .. code-block:: bash 122 | 123 | $ openxc-control write --bus 1 --id 0x124 --data 0x0234567812345678 --frame-format extended 124 | 125 | .. note:: 126 | 127 | The vehicle interface must be running firmware that supports CAN writes, and 128 | must allow writing the specific message that you request with 129 | ``openxc-control``. 130 | 131 | 132 | --------------- 133 | sd_mount_status 134 | --------------- 135 | 136 | This queries the device to see if the SD card is mounted correctly. 137 | 138 | .. code-block:: bash 139 | 140 | $ openxc-control sd_mount_status 141 | 142 | This will return 'True' if the SD card is available for writing. Otherwise, it will return 'False'. 143 | 144 | 145 | 146 | Command-line options 147 | ==================== 148 | 149 | An overview of all possible command line options can be found via 150 | ``--help``. 151 | -------------------------------------------------------------------------------- /docs/tools/dashboard.rst: -------------------------------------------------------------------------------- 1 | ==================================================================== 2 | ``openxc-dashboard`` - an ncurses UI to view data received from a VI 3 | ==================================================================== 4 | 5 | :program:`openxc-dashboard` is a command-line tool that displays the current 6 | values of all OpenXC messages simultaneously. The dashboard uses ``curses`` to 7 | draw a basic GUI to the terminal. 8 | 9 | Only OpenXC messages in the official public set will be displayed. Unofficial 10 | messages may be received, but will not appear on the dashboard. 11 | 12 | For each message type, the dashboard displays: 13 | 14 | * Message name 15 | * Last received value 16 | * A simple graph of the current value and the range seen 17 | * Total number received since the program started 18 | * A rough calculation of the frequency the message is sent in Hz 19 | 20 | If the terminal window is not wide enough, only a subset of this data will be 21 | displayed. The wider you make the window, the more you'll see. The same goes for 22 | the list of messages - if the window is not tall enough, the message list will 23 | be truncated. 24 | 25 | The dashboard also displays some overall summary data: 26 | 27 | * Total messages received of any type 28 | * Total amount of data received over the source interface 29 | * Average data rate since the program started 30 | 31 | If the number of message types is large, you can scroll up and down the list 32 | with the arrow keys or Page Up / Page Down keys. 33 | 34 | This is a screenshot of the dashboard showing all possible columns of data. 35 | 36 | .. image:: /_static/dashboard.png 37 | 38 | This screenshot shows the dashboard displaying raw CAN messages (the vehicle 39 | interface must have CAN passthrough enabled). 40 | 41 | .. image:: /_static/dashboard-raw.png 42 | 43 | Basic use 44 | ========= 45 | 46 | Open the dashboard: 47 | 48 | .. code-block:: bash 49 | 50 | $ openxc-dashboard 51 | 52 | Use a custom USB device: 53 | 54 | .. code-block:: bash 55 | 56 | $ openxc-dashboard --usb-vendor 4424 57 | 58 | Use a a vehicle interface connected via serial instead of USB: 59 | 60 | .. code-block:: bash 61 | 62 | $ openxc-dashboard --serial --serial-device /dev/ttyUSB1 63 | 64 | The ``serial-device`` option is only required if the virtual COM port is 65 | different than the default ``/dev/ttyUSB0``. 66 | 67 | Play back a trace file in real-time: 68 | 69 | .. code-block:: bash 70 | 71 | $ openxc-dashboard --trace monday-trace.json 72 | 73 | Command-line options 74 | ==================== 75 | 76 | An overview of all possible command line options can be found via 77 | ``--help``. 78 | -------------------------------------------------------------------------------- /docs/tools/diag.rst: -------------------------------------------------------------------------------- 1 | ============================================================= 2 | ``openxc-diag`` - Send and receive OBD-II diagnostic messages 3 | ============================================================= 4 | 5 | :program:`openxc-diag` is a command-line tool for adding new recurring or 6 | one-time diagnostic message requests through a vehicle interface. 7 | 8 | Perform a single diagnostic request 9 | =================================== 10 | 11 | This example will add a new one-time diagnostic request - it will be sent 12 | once, and any respones will be printed to the terminal via stdout. The 13 | ``--message`` and ``--mode`` options are required. This particular request 14 | sends a functional broadcast request (ID ``0x7df``) for the mode 3 service, to 15 | store a "freeze frame". See the Unified Diagnostics Service and On-Board 16 | Diagnostics standards for more information on valid modes. 17 | 18 | The ``bus`` option is not required; the VI wlil use its default configured CAN 19 | bus if one is not specified. 20 | 21 | .. code-block:: bash 22 | 23 | $ openxc-diag add --message 0x7df --mode 0x3 24 | 25 | .. note:: 26 | 27 | The vehicle interface must be running firmware that supports diagnostic 28 | requests. 29 | 30 | Add a recurring diagnostic request 31 | ===================================== 32 | 33 | This example will register a new recurring diagnostic request with the vehicle 34 | interface. It will request the OBD-II engine speed parameter at 1Hz, so if you 35 | subseqeuntly run the ``openxc-dump`` command you will be able to read the 36 | responses. 37 | 38 | .. code-block:: bash 39 | 40 | $ openxc-diag add --message 0x7df --mode 0x1 --pid 0xc --frequency 1 41 | 42 | Cancel an existing recurring diagnostic request 43 | =============================================== 44 | 45 | This example will cancel the recurring diagnostic request we added in the 46 | previous example. Deleting requests also uses the combination of bus, ID, mode 47 | and PID to identify a request. 48 | 49 | .. code-block:: bash 50 | 51 | $ openxc-diag cancel --message 0x7df --mode 0x1 --pid 0xc 52 | 53 | Cancelling a non-recurring diagnostic request 54 | ========================================================= 55 | 56 | If you're wondering why there are no examples of canceling an existing request 57 | that is not recurring, it's because they either complete or timeout withing 1 58 | second, so there's no reason to try and modify them. 59 | 60 | Command-line options 61 | ==================== 62 | 63 | A description overview of all possible command line options can be found via 64 | ``--help``. 65 | -------------------------------------------------------------------------------- /docs/tools/dump.rst: -------------------------------------------------------------------------------- 1 | ============================================================== 2 | ``openxc-dump`` - view the unfiltered stream of data from a VI 3 | ============================================================== 4 | 5 | :program:`openxc-dump` is a command-line tool to view the raw data stream from 6 | an attached vehicle interface or trace file. It attempts to read OpenXC messages 7 | from the interface specified at the command line (USB, Bluetooth (Linux), serial 8 | a trace file) and prints each message received to ``stdout``. 9 | 10 | Basic use 11 | ========= 12 | 13 | View everything: 14 | 15 | .. code-block:: bash 16 | 17 | $ openxc-dump 18 | 19 | View only a particular message: 20 | 21 | .. code-block:: bash 22 | 23 | $ openxc-dump | grep steering_wheel_angle 24 | 25 | 26 | Use a custom USB device: 27 | 28 | .. code-block:: bash 29 | 30 | $ openxc-dump --usb-vendor 4424 31 | 32 | Use a vehicle interface connected via serial instead of USB: 33 | 34 | .. code-block:: bash 35 | 36 | $ openxc-dump --serial --serial-device /dev/ttyUSB1 37 | 38 | The ``serial-device`` option is only required if the virtual COM port is 39 | different than the default ``/dev/ttyUSB0``. 40 | 41 | Use a VI with a Bluetooth adapter (this is only supported when connecting from 42 | Linux at the moment): 43 | 44 | .. code-block:: bash 45 | 46 | $ openxc-dump --bluetooth 47 | 48 | This will scan and discover for an OpenXC VI, connect and start streaming the 49 | data. If you know the MAC address, you can also provide that explicitly with 50 | the ``--bluetooth-address`` flag. 51 | 52 | Play back a trace file in real-time: 53 | 54 | .. code-block:: bash 55 | 56 | $ openxc-dump --trace monday-trace.json 57 | 58 | 59 | Command-line options 60 | ==================== 61 | 62 | An overview of all possible command line options can be found via 63 | ``--help``. 64 | 65 | Traces 66 | ======= 67 | 68 | You can record a trace of JSON messages from the CAN reader with 69 | ``openxc-dump``. Simply redirect the output to a file, and you've got your 70 | trace. This can be used directly by the openxc-android library, for example. 71 | 72 | .. code-block:: bash 73 | 74 | $ openxc-dump > vehicle-data.trace 75 | -------------------------------------------------------------------------------- /docs/tools/gps.rst: -------------------------------------------------------------------------------- 1 | ============================================================ 2 | ``openxc-gps`` - convert a vehicle data stream to GPX format 3 | ============================================================ 4 | 5 | :program:`openxc-gps` is a command-line tool to convert a raw OpenXC data stream 6 | that includes GPS information (namely latitude and longitude) into one of a few 7 | popular formats for GPS traces. The output file is printed to `stdout`, so the 8 | output must be redirected to save it to a file. 9 | 10 | The only format currently supported is `.gpx`, which can be imported by 11 | Google Earth, the Google Maps API and many other popular tools. 12 | 13 | Basic use 14 | ========= 15 | 16 | Convert a previously recorded OpenXC JSON trace file to GPX: 17 | 18 | .. code-block:: bash 19 | 20 | $ openxc-gps --trace trace.json > trace.gpx 21 | 22 | Convert a real-time stream from a USB vehicle interface to GPX in real-time (using 23 | all defaults, and printing to `stdout`): 24 | 25 | .. code-block:: bash 26 | 27 | $ openxc-gps 28 | 29 | Command-line options 30 | ==================== 31 | 32 | An overview of all possible command line options can be found via 33 | ``--help``. 34 | -------------------------------------------------------------------------------- /docs/tools/obd2scanner.rst: -------------------------------------------------------------------------------- 1 | ================================================================== 2 | ``openxc-obd2scanner`` - detect OBD-II PIDs supported by a vehicle 3 | ================================================================== 4 | 5 | :program:`openxc-obd2scanner` is a simple and quick tool to check what OBD-II 6 | PIDs a vehicle actually supports. It sequentially scans all valid PIDs and 7 | prints the responses to stdout. 8 | 9 | Basic use 10 | ========= 11 | 12 | .. code-block:: bash 13 | 14 | $ openxc-obd2scanner 15 | 16 | Command-line options 17 | ==================== 18 | 19 | A description overview of all possible command line options can be found via 20 | ``--help``. 21 | -------------------------------------------------------------------------------- /docs/tools/scanner.rst: -------------------------------------------------------------------------------- 1 | ======================================================================== 2 | ``openxc-scanner`` - scanner for determining support diagnostic requests 3 | ======================================================================== 4 | 5 | :program:`openxc-scanner` is a is a rudimentary diagnostic scanner that can give 6 | you a high level view of the what message IDs are used by modules on a vehicle 7 | network and to which diagnostics services they (potentially) respond. 8 | 9 | When you run ``openxc-scanner``, it will send a Tester Present diagnostic 10 | request to all possible 11-bit CAN message IDs (or arbitration IDs). For each 11 | module that responds, it then sends a blank request for each possible diagnostic 12 | service to the module's arbitration ID. Finally, for each service that 13 | responded, it fuzzes the payload field to see if anything interesting can 14 | happen. 15 | 16 | Make sure you do not run this tool while operating your car. The Tester Present 17 | message can put modules into diagnostic modes that aren't safe for driving, or 18 | other unexpected behaviors may occur (e.g. your powered driver's seat may reset 19 | the position, or the powered trunk may open up). 20 | 21 | Basic use 22 | ========= 23 | 24 | There's not much to it, just run it and view the results. It may take a number 25 | of minutes to complete the scan if there are many active modules. 26 | 27 | .. code-block:: bash 28 | 29 | $ openxc-scanner 30 | 31 | Scanning a specific message ID 32 | ============================== 33 | 34 | If you wish to scan only a single message ID, you can skip right to it: 35 | 36 | .. code-block:: bash 37 | 38 | $ openxc-scanner --message 0x7e0 39 | 40 | Command-line options 41 | ==================== 42 | 43 | A description overview of all possible command line options can be found via 44 | ``--help``. 45 | -------------------------------------------------------------------------------- /docs/tools/tools.rst: -------------------------------------------------------------------------------- 1 | Command Line Tools 2 | =================== 3 | 4 | With all tools, the library will attempt to autodetect the payload format 5 | being used by the VI. If it's not sending any messages this is not possible, so 6 | you may need to provide the current payload format explicitly with the 7 | ``--format`` flag. For example, here's a command to change the 8 | passthrough status of bus 1, but with the payload format for the request 9 | explicitly set the protocol buffers: 10 | 11 | .. code-block:: bash 12 | 13 | $ openxc-control set --bus 1 --passthrough --format protobuf 14 | 15 | The following links describe the available openxc-python commands. 16 | 17 | .. toctree:: 18 | :maxdepth: 1 19 | :glob: 20 | 21 | * 22 | -------------------------------------------------------------------------------- /docs/tools/tracesplit.rst: -------------------------------------------------------------------------------- 1 | ============================================================================ 2 | ``openxc-trace-split`` - split merged OpenXC trace files into separate trips 3 | ============================================================================ 4 | 5 | :program:`openxc-trace-split` is a command-line tool to re-split a collection of 6 | previously recorded OpenXC trace files by different units of time. 7 | 8 | Often, trace files are recorded into arbitrarily sized chunks, e.g. a new trace 9 | file every hour. The trace files are often most useful if grouped into more 10 | logical chunks e.g. one "trip" in the vehicle. 11 | 12 | This tool accepts a list of JSON trace files as arguments, reads them into 13 | memory and sorts by time, then re-splits the file into new output files based on 14 | the requested split unit. The unit is "trips" by default, which looks for gaps 15 | of 5 minutes or more in the trace files to demarcate the trips. 16 | 17 | The output files are named based on the timestamp of the first record recorded 18 | in the segment. 19 | 20 | Basic use 21 | ========= 22 | 23 | Re-combine two trace files and re-split by trip (the default split unit) 24 | instead of the original day splits: 25 | 26 | .. code-block:: bash 27 | 28 | $ openxc-trace-split monday.json tuesday.json 29 | 30 | Re-combine two trace files and re-split by hour instead of the original day 31 | splits: 32 | 33 | .. code-block:: bash 34 | 35 | $ openxc-trace-split --split hour monday.json tuesday.json 36 | 37 | Re-split an entire directory of JSON files by trip 38 | 39 | .. code-block:: bash 40 | 41 | $ openxc-trace-split *.json 42 | 43 | Command-line options 44 | ==================== 45 | 46 | A quick overview of all possible command line options can be found via 47 | ``--help``. 48 | 49 | .. cmdoption:: -s, --split 50 | 51 | Change the time unit used to split trace files - choices are ``day``, 52 | ``hour`` and ``trip``. The default unit is ``trip``, which looks for large 53 | gaps of time in the trace files where no data was recorded. 54 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import os 4 | from fabric.api import * 5 | from fabric.colors import green, yellow 6 | from fabric.contrib.console import confirm 7 | from prettyprint import pp 8 | 9 | VERSION_PATTERN = r'^v\d+(\.\d+)+?$' 10 | 11 | 12 | @task 13 | def browse(): 14 | """ 15 | Open the current dev docs in a browser tab. 16 | """ 17 | local("$BROWSER docs/_build/html/index.html") 18 | 19 | 20 | @task(default=True) 21 | def test(args=None): 22 | local("tox") 23 | 24 | 25 | def latest_git_tag(): 26 | description = local('git describe master', capture=True).rstrip('\n') 27 | if '-' in description: 28 | latest_tag = description[:description.find('-')] 29 | else: 30 | latest_tag = description 31 | if not re.match(VERSION_PATTERN, latest_tag): 32 | latest_tag = None 33 | return latest_tag 34 | 35 | 36 | def compare_versions(x, y): 37 | """ 38 | Expects 2 strings in the format of 'X.Y.Z' where X, Y and Z are 39 | integers. It will compare the items which will organize things 40 | properly by their major, minor and bugfix version. 41 | :: 42 | 43 | >>> my_list = ['v1.13', 'v1.14.2', 'v1.14.1', 'v1.9', 'v1.1'] 44 | >>> sorted(my_list, cmp=compare_versions) 45 | ['v1.1', 'v1.9', 'v1.13', 'v1.14.1', 'v1.14.2'] 46 | 47 | """ 48 | def version_to_tuple(version): 49 | # Trim off the leading v 50 | version_list = version[1:].split('.', 2) 51 | if len(version_list) <= 3: 52 | [version_list.append(0) for _ in range(3 - len(version_list))] 53 | try: 54 | return tuple((int(version) for version in version_list)) 55 | except ValueError: # not an integer, so it goes to the bottom 56 | return (0, 0, 0) 57 | 58 | x_major, x_minor, x_bugfix = version_to_tuple(x) 59 | y_major, y_minor, y_bugfix = version_to_tuple(y) 60 | return (cmp(x_major, y_major) or cmp(x_minor, y_minor) 61 | or cmp(x_bugfix, y_bugfix)) 62 | 63 | 64 | def make_tag(): 65 | if confirm(yellow("Tag this release?"), default=True): 66 | print((green("The last 5 tags were: "))) 67 | tags = local('git tag | tail -n 20', capture=True) 68 | pp(sorted(tags.split('\n'), compare_versions, reverse=True)) 69 | prompt("New release tag in the format vX.Y[.Z]?", 'tag', 70 | validate=VERSION_PATTERN) 71 | local('git tag -as %(tag)s' % env) 72 | local('git push origin', capture=True) 73 | local('git push --tags origin', capture=True) 74 | local('git fetch --tags origin', capture=True) 75 | else: 76 | env.tag = latest_git_tag() 77 | print((green("Using latest tag %(tag)s" % env))) 78 | return env.tag 79 | 80 | @task 81 | def coverage(): 82 | local('nosetests --with-coverage') 83 | 84 | @task 85 | def diffcover(): 86 | coverage() 87 | local('coverage xml') 88 | local('diff-cover --compare-branch=origin/next coverage.xml --html-report diffcover.html') 89 | 90 | @task 91 | def release(): 92 | """ 93 | Build, register and upload to PyPI 94 | """ 95 | test() 96 | tag = make_tag() 97 | 98 | puts("Uploading to PyPI") 99 | local('python setup.py sdist register upload') 100 | green("Uploaded %s to PyPI" % tag) 101 | 102 | -------------------------------------------------------------------------------- /openxc/__init__.py: -------------------------------------------------------------------------------- 1 | import openxc.logconfig 2 | -------------------------------------------------------------------------------- /openxc/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Controller, ControllerError 2 | from .usb import UsbControllerMixin 3 | from .serial import SerialControllerMixin 4 | -------------------------------------------------------------------------------- /openxc/controllers/serial.py: -------------------------------------------------------------------------------- 1 | """Controller implementation for a virtual serial device.""" 2 | 3 | import time 4 | 5 | from .base import Controller 6 | 7 | 8 | class SerialControllerMixin(Controller): 9 | """An implementation of a Controller type that connects to a virtual serial 10 | device. 11 | 12 | This class acts as a mixin, and expects ``self.device`` to be an instance 13 | of ``serial.Serial``. 14 | 15 | TODO Bah, this is kind of weird. refactor the relationship between 16 | sources/controllers. 17 | """ 18 | 19 | WAITIED_FOR_CONNECTION = False 20 | 21 | def write_bytes(self, data): 22 | return self.device.write(data) 23 | 24 | def complex_request(self, request, blocking=True): 25 | if not self.WAITIED_FOR_CONNECTION: 26 | # TODO need to wait until device is connected or errors out 27 | # that may be a bug in the bluetooth stack, see 28 | # https://bugzilla.redhat.com/show_bug.cgi?id=1060457 29 | time.sleep(5) 30 | return super(SerialControllerMixin, self).complex_request(request, blocking) 31 | -------------------------------------------------------------------------------- /openxc/controllers/usb.py: -------------------------------------------------------------------------------- 1 | """Controller implementation for an OpenXC USB device.""" 2 | 3 | 4 | import logging 5 | import usb.core 6 | 7 | from .base import Controller, ControllerError 8 | 9 | LOG = logging.getLogger(__name__) 10 | 11 | 12 | class UsbControllerMixin(Controller): 13 | """An implementation of a Controller type that connects to an OpenXC USB 14 | device. 15 | 16 | This class acts as a mixin, and expects ``self.device`` to be an instance 17 | of ``usb.Device``. 18 | 19 | TODO bah, this is kind of weird. refactor the relationship between 20 | sources/controllers. 21 | """ 22 | COMPLEX_CONTROL_COMMAND = 0x83 23 | 24 | def write_bytes(self, data): 25 | if self.out_endpoint is None: 26 | LOG.warn("Can't write \"%s\" to USB, OUT endpoint is %x", data, 27 | self.out_endpoint) 28 | return 0 29 | else: 30 | return self.out_endpoint.write(data) 31 | 32 | def _send_complex_request(self, request): 33 | """Send a request via the USB control request endpoint, rather than as a 34 | bulk transfer. 35 | """ 36 | # LOG.warn("DEBUG STUFF ________________ " + str(self.streamer.serialize_for_stream(request))) 37 | self.device.ctrl_transfer(0x40, self.COMPLEX_CONTROL_COMMAND, 0, 0, 38 | self.streamer.serialize_for_stream(request)) 39 | 40 | @property 41 | def out_endpoint(self): 42 | """Open a reference to the USB device's only OUT endpoint. This method 43 | assumes that the USB device configuration has already been set. 44 | """ 45 | if getattr(self, '_out_endpoint', None) is None: 46 | config = self.device.get_active_configuration() 47 | interface_number = config[(0, 0)].bInterfaceNumber 48 | interface = usb.util.find_descriptor(config, 49 | bInterfaceNumber=interface_number) 50 | 51 | self._out_endpoint = usb.util.find_descriptor(interface, 52 | custom_match = \ 53 | lambda e: \ 54 | usb.util.endpoint_direction(e.bEndpointAddress) == \ 55 | usb.util.ENDPOINT_OUT) 56 | 57 | if not self._out_endpoint: 58 | raise ControllerError( 59 | "Couldn't find OUT endpoint on the USB device") 60 | return self._out_endpoint 61 | -------------------------------------------------------------------------------- /openxc/formats/__init__.py: -------------------------------------------------------------------------------- 1 | from .json import JsonFormatter 2 | -------------------------------------------------------------------------------- /openxc/formats/base.py: -------------------------------------------------------------------------------- 1 | class VehicleMessageStreamer(object): 2 | message_buffer = b"" 3 | bytes_received = 0 4 | 5 | def receive(self, payload): 6 | if len(payload) > 0: 7 | self.message_buffer += payload 8 | self.bytes_received += len(payload) 9 | -------------------------------------------------------------------------------- /openxc/formats/json.py: -------------------------------------------------------------------------------- 1 | """JSON formatting utilities.""" 2 | 3 | 4 | import json 5 | 6 | from openxc.formats.base import VehicleMessageStreamer 7 | 8 | class JsonStreamer(VehicleMessageStreamer): 9 | SERIALIZED_COMMAND_TERMINATOR = b"\x00" 10 | 11 | def parse_next_message(self): 12 | parsed_message = None 13 | remainder = self.message_buffer 14 | message = "" 15 | if self.SERIALIZED_COMMAND_TERMINATOR in self.message_buffer: 16 | message, _, remainder = self.message_buffer.partition( 17 | self.SERIALIZED_COMMAND_TERMINATOR) 18 | try: 19 | parsed_message = JsonFormatter.deserialize(message) 20 | if not isinstance(parsed_message, dict): 21 | raise ValueError() 22 | except ValueError: 23 | pass 24 | self.message_buffer = remainder 25 | return parsed_message 26 | 27 | def serialize_for_stream(self, message): 28 | return JsonFormatter.serialize( 29 | message) + self.SERIALIZED_COMMAND_TERMINATOR 30 | 31 | class JsonFormatter(object): 32 | 33 | @classmethod 34 | def deserialize(cls, message): 35 | return json.loads(message) 36 | 37 | @classmethod 38 | def serialize(cls, data): 39 | return json.dumps(data).encode("utf8") 40 | 41 | @classmethod 42 | def _validate(cls, message): 43 | """Confirm the validitiy of a given dict as an OpenXC message. 44 | 45 | Returns: 46 | ``True`` if the message contains at least a ``name`` and ``value``. 47 | """ 48 | valid = False 49 | if(('name' in message and 'value' in message) or 50 | ('id' in message and 'data' in message)): 51 | valid = True 52 | return valid 53 | -------------------------------------------------------------------------------- /openxc/generator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openxc/openxc-python/5341180fea6c364027dedc9bc4c8027b2831325f/openxc/generator/__init__.py -------------------------------------------------------------------------------- /openxc/generator/signals.cpp.footer: -------------------------------------------------------------------------------- 1 | CanCommand* openxc::signals::getCommands() { 2 | return COMMANDS[getActiveMessageSet()->index]; 3 | } 4 | 5 | int openxc::signals::getCommandCount() { 6 | return getActiveMessageSet()->commandCount; 7 | } 8 | 9 | const CanMessageDefinition* openxc::signals::getMessages() { 10 | return CAN_MESSAGES[getActiveMessageSet()->index]; 11 | } 12 | 13 | int openxc::signals::getMessageCount() { 14 | return getActiveMessageSet()->messageCount; 15 | } 16 | 17 | const CanSignal* openxc::signals::getSignals() { 18 | return SIGNALS[getActiveMessageSet()->index]; 19 | } 20 | 21 | SignalManager* openxc::signals::getSignalManagers() { 22 | return SIGNAL_MANAGERS[getActiveMessageSet()->index]; 23 | } 24 | 25 | int openxc::signals::getSignalCount() { 26 | return getActiveMessageSet()->signalCount; 27 | } 28 | 29 | CanBus* openxc::signals::getCanBuses() { 30 | return CAN_BUSES[getActiveMessageSet()->index]; 31 | } 32 | 33 | int openxc::signals::getCanBusCount() { 34 | return getActiveMessageSet()->busCount; 35 | } 36 | 37 | const CanMessageSet* openxc::signals::getActiveMessageSet() { 38 | return &MESSAGE_SETS[getConfiguration()->messageSetIndex]; 39 | } 40 | 41 | const CanMessageSet* openxc::signals::getMessageSets() { 42 | return MESSAGE_SETS; 43 | } 44 | 45 | int openxc::signals::getMessageSetCount() { 46 | return MESSAGE_SET_COUNT; 47 | } 48 | -------------------------------------------------------------------------------- /openxc/generator/signals.cpp.header: -------------------------------------------------------------------------------- 1 | /* DO NOT MODIFY: This source is generated by the scripts in the 2 | * vi-firmware repository. 3 | * 4 | * Generated for v{0} of the OpenXC VI firmware. 5 | */ 6 | 7 | #include "diagnostics.h" 8 | #include "can/canread.h" 9 | #include "can/canwrite.h" 10 | #include "signals.h" 11 | #include "obd2.h" 12 | #include "util/log.h" 13 | #include "config.h" 14 | #include "shared_handlers.h" 15 | 16 | namespace can = openxc::can; 17 | 18 | using openxc::util::log::debug; 19 | using openxc::util::time::FrequencyClock; 20 | using openxc::pipeline::Pipeline; 21 | using openxc::config::getConfiguration; 22 | using openxc::can::read::booleanDecoder; 23 | using openxc::can::read::stateDecoder; 24 | using openxc::can::read::ignoreDecoder; 25 | using openxc::diagnostics::obd2::handleObd2Pid; 26 | using namespace openxc::signals::handlers; 27 | -------------------------------------------------------------------------------- /openxc/generator/xml_to_json.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from openxc.utils import fatal_error 4 | from .structures import Signal, Message 5 | 6 | LOG = logging.getLogger(__name__) 7 | 8 | class Network(object): 9 | """Represents all the messages on a single bus in an XML-backed database.""" 10 | 11 | def __init__(self, database_name, tree, all_messages): 12 | self.messages = {} 13 | 14 | for message_id, message in list(all_messages.items()): 15 | numeric_message_id = int(message_id, 0) 16 | query = "./Node/TxMessage[ID=\"0x%s\"]" 17 | # Search for both lower and upper case hex 18 | for attr_value in ["%X", "%x"]: 19 | node = tree.find(query % (attr_value % numeric_message_id)) 20 | if node is not None: 21 | break 22 | if node is None: 23 | LOG.warning("Unable to find message ID 0x%x in %s" % ( 24 | numeric_message_id, database_name)) 25 | else: 26 | if 'signals' not in message: 27 | fatal_error("This message object is missing a " 28 | "top-level 'signals' field: %s" % message) 29 | self.messages[numeric_message_id] = XMLBackedMessage.from_xml_node( 30 | node, message_id, message['signals']) 31 | 32 | def to_dict(self): 33 | return {'messages': dict(("%s" % message.string_id, 34 | message.to_dict()) 35 | for message in list(self.messages.values()) 36 | if len(message.signals) > 0)} 37 | 38 | class XMLBackedMessage(Message): 39 | 40 | @classmethod 41 | def from_xml_node(cls, node, string_message_id, mapped_signals): 42 | message = cls() 43 | 44 | message.name = node.find("Name").text 45 | # Keep around whatever string is used in the mapping, so our dict merge 46 | # works with case insensitive key matching 47 | message.string_id = string_message_id 48 | message.id = int(node.find("ID").text, 0) 49 | 50 | for signal_name, mapped_signal in mapped_signals.items(): 51 | mapped_signal_node = node.find("Signal[Name=\"%s\"]" % signal_name) 52 | if mapped_signal_node is not None: 53 | signal = XMLBackedSignal.from_xml_node(mapped_signal_node) 54 | signal.generic_name = mapped_signal.get('generic_name', None) or signal_name 55 | message.signals[signal_name] = signal 56 | 57 | return message 58 | 59 | class XMLBackedSignal(Signal): 60 | 61 | @classmethod 62 | def from_xml_node(cls, node): 63 | """Construct a Signal instance from an XML node exported from a Vector 64 | CANoe .dbc file.""" 65 | return cls(name=node.find("Name").text, 66 | bit_position=int(node.find("Bitposition").text), 67 | bit_size=int(node.find("Bitsize").text), 68 | factor=float(node.find("Factor").text), 69 | offset=float(node.find("Offset").text), 70 | min_value=float(node.find("Minimum").text), 71 | max_value=float(node.find("Maximum").text)) 72 | 73 | 74 | def parse_database(database_filename): 75 | try: 76 | from lxml import etree 77 | except ImportError: 78 | LOG.warning("Install the 'lxml' Python package to speed up CAN database parsing") 79 | try: 80 | # Python 2.5 81 | import xml.etree.cElementTree as etree 82 | except ImportError: 83 | try: 84 | # Python 2.5 85 | import xml.etree.ElementTree as etree 86 | except ImportError: 87 | try: 88 | # normal cElementTree install 89 | import cElementTree as etree 90 | except ImportError: 91 | try: 92 | # normal ElementTree install 93 | import elementtree.ElementTree as etree 94 | except ImportError: 95 | fatal_error("Failed to import ElementTree from any known place") 96 | return etree.parse(database_filename) 97 | 98 | 99 | def merge_database_into_mapping(database_filename, database_tree, messages): 100 | if len(messages) == 0: 101 | LOG.warning("No messages specified for mapping from XML") 102 | return {} 103 | else: 104 | return Network(database_filename, database_tree, messages).to_dict() 105 | -------------------------------------------------------------------------------- /openxc/interface.py: -------------------------------------------------------------------------------- 1 | """Combinations of DataSource with Controller types for duplex vehicle 2 | interfaces.""" 3 | from .sources import UsbDataSource, SerialDataSource, NetworkDataSource, \ 4 | BluetoothVehicleInterface 5 | from .controllers import UsbControllerMixin, SerialControllerMixin, Controller 6 | 7 | 8 | class UsbVehicleInterface(UsbDataSource, UsbControllerMixin): 9 | """This class is compatibile with an OpenXC vehicle interface vehicle interface 10 | attached via USB. It supports full duplex reads and writes. 11 | """ 12 | pass 13 | 14 | class SerialVehicleInterface(SerialDataSource, SerialControllerMixin): 15 | """This class is compatibile with an OpenXC vehicle interface vehicle interface 16 | connected via a virtual serial port (e.g. FTDI or Bluetooth). It has full 17 | read support and limited write support (no control commands are supported). 18 | """ 19 | pass 20 | 21 | 22 | -------------------------------------------------------------------------------- /openxc/logconfig.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | try: 4 | from logging import NullHandler 5 | except ImportError: 6 | class NullHandler(logging.Handler): 7 | def emit(self, record): 8 | pass 9 | 10 | logging.getLogger("openxc").addHandler(NullHandler()) 11 | -------------------------------------------------------------------------------- /openxc/sinks/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import DataSink 2 | from .queued import QueuedSink 3 | from .notifier import MeasurementNotifierSink 4 | from .recorder import FileRecorderSink 5 | from .uploader import UploaderSink 6 | -------------------------------------------------------------------------------- /openxc/sinks/base.py: -------------------------------------------------------------------------------- 1 | """Common operations for all vehicle data sinks.""" 2 | 3 | class DataSink(object): 4 | """A base interface for all data sinks. At the minimum, a data sink must 5 | have a :func:`receive` method. 6 | """ 7 | 8 | def receive(self, message, **kwargs): 9 | """Handle an incoming vehicle data message. 10 | 11 | Args: 12 | message (dict) - a new OpenXC vehicle data message 13 | 14 | Kwargs: 15 | data_remaining (bool) - if the originating data source can peek ahead 16 | in the data stream, this argument will True if there is more data 17 | available. 18 | """ 19 | raise NotImplementedError("Don't use DataSink directly") 20 | -------------------------------------------------------------------------------- /openxc/sinks/notifier.py: -------------------------------------------------------------------------------- 1 | """A data sink implementation for the core listener notification service of 2 | :class:`openxc.vehicle.Vehicle`. 3 | """ 4 | from threading import Thread 5 | from collections import defaultdict 6 | import logging 7 | 8 | from openxc.measurements import Measurement, UnrecognizedMeasurementError 9 | from .queued import QueuedSink 10 | 11 | LOG = logging.getLogger(__name__) 12 | 13 | 14 | class MeasurementNotifierSink(QueuedSink): 15 | """Notify previously registered callbacks whenever measurements of a certian 16 | type have been received. 17 | 18 | This data sink is the core of the asynchronous interface of 19 | :class:`openxc.vehicle.Vehicle.` 20 | """ 21 | def __init__(self): 22 | super(MeasurementNotifierSink, self).__init__() 23 | self.callbacks = defaultdict(set) 24 | self.notifier = self.Notifier(self.queue, self._propagate) 25 | 26 | def register(self, measurement_class, callback): 27 | """Call the ``callback`` with any new values of ``measurement_class`` 28 | received. 29 | """ 30 | self.callbacks[Measurement.name_from_class(measurement_class) 31 | ].add(callback) 32 | 33 | def unregister(self, measurement_class, callback): 34 | """Stop notifying ``callback`` of new values of ``measurement_class``. 35 | 36 | If the callback wasn't previously registered, this method will have no 37 | effect. 38 | """ 39 | self.callbacks[Measurement.name_from_class(measurement_class) 40 | ].remove(callback) 41 | 42 | def _propagate(self, measurement, **kwargs): 43 | measurement_callbacks = self.callbacks[measurement.name] 44 | measurement_callbacks.update(self.callbacks[Measurement.name]) 45 | for callback in measurement_callbacks: 46 | try: 47 | callback(measurement, **kwargs) 48 | except TypeError: 49 | callback(measurement) 50 | 51 | class Notifier(Thread): 52 | def __init__(self, queue, callback): 53 | super(MeasurementNotifierSink.Notifier, self).__init__() 54 | self.daemon = True 55 | self.queue = queue 56 | self.callback = callback 57 | self.start() 58 | 59 | def run(self): 60 | while True: 61 | message, kwargs = self.queue.get() 62 | try: 63 | measurement = Measurement.from_dict(message) 64 | self.callback(measurement, **kwargs) 65 | self.queue.task_done() 66 | except UnrecognizedMeasurementError as e: 67 | LOG.warn(e) 68 | -------------------------------------------------------------------------------- /openxc/sinks/queued.py: -------------------------------------------------------------------------------- 1 | """Common functinality for data sinks that work on a queue of incoming 2 | messages. 3 | """ 4 | from .base import DataSink 5 | 6 | try: 7 | from queue import Queue 8 | except ImportError: 9 | # Python 3 10 | from queue import Queue 11 | 12 | class QueuedSink(DataSink): 13 | """Store every message received and any kwargs from the originating data 14 | source as a tuple in a queue. 15 | 16 | The queue can be reference in subclasses via the `queue` attribute. 17 | """ 18 | def __init__(self): 19 | super(QueuedSink, self).__init__() 20 | self.queue = Queue() 21 | 22 | def receive(self, message, **kwargs): 23 | """Add the `message` and `kwargs` to the queue.""" 24 | self.queue.put((message, kwargs)) 25 | -------------------------------------------------------------------------------- /openxc/sinks/recorder.py: -------------------------------------------------------------------------------- 1 | """Trace file recording operations.""" 2 | from threading import Thread 3 | import datetime 4 | import time 5 | 6 | from openxc.formats import JsonFormatter 7 | from .queued import QueuedSink 8 | 9 | 10 | class FileRecorderSink(QueuedSink): 11 | """A sink to record trace files based on the messages received from all data 12 | sources. 13 | """ 14 | FILENAME_DATE_FORMAT = "%Y-%m-%d-%H" 15 | FILENAME_FORMAT = "%s.json" 16 | 17 | def __init__(self): 18 | super(FileRecorderSink, self).__init__() 19 | self.recorder = self.Recorder(self.queue) 20 | 21 | class Recorder(Thread): 22 | def __init__(self, queue): 23 | super(FileRecorderSink.Recorder, self).__init__() 24 | self.daemon = True 25 | self.queue = queue 26 | self.start() 27 | 28 | @staticmethod 29 | def _generate_filename(): 30 | current_date = datetime.datetime.now() 31 | return FileRecorderSink.FILENAME_FORMAT % current_date.strftime( 32 | FileRecorderSink.FILENAME_DATE_FORMAT) 33 | 34 | def run(self): 35 | while True: 36 | last_hour_opened = datetime.datetime.now().hour 37 | filename = self._generate_filename() 38 | with open(filename, 'a') as output_file: 39 | while True: 40 | message, _ = self.queue.get() 41 | message['timestamp'] = time.time() 42 | output_file.write(JsonFormatter.serialize(message)) 43 | self.queue.task_done() 44 | 45 | if datetime.datetime.now().hour > last_hour_opened: 46 | break 47 | -------------------------------------------------------------------------------- /openxc/sinks/uploader.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | import time 3 | import logging 4 | import requests 5 | 6 | from openxc.formats import JsonFormatter 7 | from .queued import QueuedSink 8 | 9 | LOG = logging.getLogger(__name__) 10 | 11 | 12 | class UploaderSink(QueuedSink): 13 | """Uploads all incoming vehicle data to a remote web application via HTTP. 14 | 15 | TODO document service side format 16 | """ 17 | UPLOAD_BATCH_SIZE = 25 18 | HTTP_TIMEOUT = 5000 19 | 20 | def __init__(self, url): 21 | """Args: 22 | url (str) - the URL to send an HTTP POST request with vehicle data 23 | """ 24 | super(UploaderSink, self).__init__() 25 | self.recorder = self.Uploader(self.queue, url) 26 | 27 | class Uploader(Thread): 28 | def __init__(self, queue, url): 29 | super(UploaderSink.Uploader, self).__init__() 30 | self.daemon = True 31 | self.queue = queue 32 | self.records = [] 33 | self.url = url 34 | self.start() 35 | 36 | @classmethod 37 | def _upload(cls, url, records): 38 | payload = JsonFormatter.serialize(records) 39 | response = requests.post(url, data=payload) 40 | 41 | if response.status_code != requests.codes.created: 42 | LOG.warn("Unable to upload %d records, received %d status " 43 | "from %s", len(records), response.status_code, url) 44 | else: 45 | LOG.debug("Uploaded %d records (status %d)", len(records), 46 | response.status_code) 47 | 48 | def run(self): 49 | while True: 50 | message, _ = self.queue.get() 51 | 52 | message['timestamp'] = time.time() 53 | self.records.append(message) 54 | if len(self.records) > UploaderSink.UPLOAD_BATCH_SIZE: 55 | self._upload(self.url, self.records) 56 | 57 | self.queue.task_done() 58 | -------------------------------------------------------------------------------- /openxc/sources/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import DataSource, DataSourceError 2 | from .usb import UsbDataSource 3 | from .serial import SerialDataSource 4 | from .trace import TraceDataSource 5 | from .network import NetworkDataSource 6 | from .bluetooth import BluetoothVehicleInterface 7 | -------------------------------------------------------------------------------- /openxc/sources/base.py: -------------------------------------------------------------------------------- 1 | """Abstract base interface for vehicle data sources.""" 2 | 3 | 4 | import threading 5 | import logging 6 | import string 7 | import sys 8 | import datetime 9 | 10 | from openxc.formats.binary import ProtobufStreamer, ProtobufFormatter 11 | from openxc.formats.json import JsonStreamer, JsonFormatter 12 | 13 | LOG = logging.getLogger(__name__) 14 | 15 | class MissingPayloadFormatError(Exception): pass 16 | 17 | class DataSource(threading.Thread): 18 | """Interface for all vehicle data sources. This inherits from Thread and 19 | when a source is added to a vehicle it attempts to call the ``start()`` 20 | method if it exists. If an implementer of DataSource needs some background 21 | process to read data, it's just a matter of defining a ``run()`` method. 22 | 23 | A data source requires a callback method to be specified. Whenever new data 24 | is received, it will pass it to that callback. 25 | """ 26 | def __init__(self, callback=None, log_mode=None, payload_format=None): 27 | """Construct a new DataSource. 28 | 29 | By default, DataSource threads are marked as ``daemon`` threads, so they 30 | will die as soon as all other non-daemon threads in the process have 31 | quit. 32 | 33 | Kwargs: 34 | callback - function to call with any new data received 35 | """ 36 | super(DataSource, self).__init__() 37 | self.callback = callback 38 | self.daemon = True 39 | self.running = True 40 | self._streamer = None 41 | self._formatter = None 42 | self._format = payload_format 43 | self.format = payload_format # Added 7/30/2021 to fix protobuf streaming out 44 | 45 | self.logger = SourceLogger(self, log_mode) 46 | 47 | @property 48 | def format(self): 49 | return self._format 50 | 51 | @format.setter 52 | def format(self, value): 53 | self._format = value 54 | if value == "json": 55 | self.streamer = JsonStreamer() 56 | self.formatter = JsonFormatter 57 | elif value == "protobuf": 58 | self.streamer = ProtobufStreamer() 59 | self.formatter = ProtobufFormatter 60 | 61 | @property 62 | def streamer(self): 63 | if self._streamer is None: 64 | raise MissingPayloadFormatError("Unable to auto-detect payload " 65 | "format, must specify manually with --format [json|protobuf]") 66 | return self._streamer 67 | 68 | @streamer.setter 69 | def streamer(self, value): 70 | self._streamer = value 71 | 72 | @property 73 | def formatter(self): 74 | if self._formatter is None: 75 | raise MissingPayloadFormatError("Unable to auto-detect payload " 76 | "format, must specify manually with --format [json|protobuf]") 77 | return self._formatter 78 | 79 | @formatter.setter 80 | def formatter(self, value): 81 | self._formatter = value 82 | 83 | @property 84 | def bytes_received(self): 85 | return self.streamer.bytes_received 86 | 87 | def start(self): 88 | self.logger.start() 89 | super(DataSource, self).start() 90 | 91 | def stop(self): 92 | self.logger.stop() 93 | self.running = False 94 | 95 | def read(self, timeout=None): 96 | """Read data from the source. 97 | 98 | Kwargs: 99 | timeout (float) - if the source implementation could potentially 100 | block, timeout after this number of seconds. 101 | """ 102 | raise NotImplementedError("Don't use DataSource directly") 103 | 104 | def read_logs(self, timeout=None): 105 | """Read log data from the source. 106 | 107 | Kwargs: 108 | timeout (float) - if the source implementation could potentially 109 | block, timeout after this number of seconds. 110 | """ 111 | raise NotImplementedError("Don't use DataSource directly") 112 | 113 | 114 | class SourceLogger(threading.Thread): 115 | FILENAME_TEMPLATE = "%d-%m-%Y.%H-%M-%S" 116 | 117 | def __init__(self, source, mode="off"): 118 | super(SourceLogger, self).__init__() 119 | self.daemon = True 120 | self.source = source 121 | self.mode = mode 122 | self.file = None 123 | self.running = True 124 | 125 | if self.mode == "file": 126 | filename = "openxc-logs-%s.txt" % datetime.datetime.now().strftime( 127 | self.FILENAME_TEMPLATE) 128 | self.file = open(filename, 'w') 129 | 130 | def stop(self): 131 | self.running = False 132 | 133 | def record(self, message): 134 | if self.mode is not None and self.mode != "off" and len(message) > 0: 135 | log_file = None 136 | if self.mode == "stderr": 137 | log_file = sys.stderr 138 | elif self.mode == "file" and self.file is not None: 139 | log_file = self.file 140 | print("LOG: %s" % message, file=log_file) 141 | 142 | def run(self): 143 | """Continuously read data from the source and attempt to parse a valid 144 | message from the buffer of bytes. When a message is parsed, passes it 145 | off to the callback if one is set. 146 | """ 147 | message_buffer = "" 148 | while self.running: 149 | try: 150 | message_buffer += self.source.read_logs() 151 | except DataSourceError as e: 152 | if self.running: 153 | LOG.warn("Can't read logs from data source -- stopping: %s", e) 154 | break 155 | except NotImplementedError as e: 156 | LOG.info("%s doesn't support logging" % self) 157 | break 158 | 159 | while True: 160 | if "\x00" not in message_buffer: 161 | break 162 | record, _, remainder = message_buffer.partition("\x00") 163 | self.record(record) 164 | message_buffer = remainder 165 | 166 | 167 | class BytestreamDataSource(DataSource): 168 | """A source that receives data is a series of bytes, with discrete messages 169 | separated by a newline character. 170 | 171 | Subclasses of this class need only to implement the ``read`` method. 172 | """ 173 | 174 | def __init__(self, **kwargs): 175 | super(BytestreamDataSource, self).__init__(**kwargs) 176 | self.corrupted_messages = 0 177 | self.running = True 178 | 179 | def _message_valid(self, message): 180 | if not hasattr(message, '__iter__'): 181 | return False 182 | if not ('name' in message and 'value' in message or 183 | ('id' in message and 'data' in message) or 184 | ('id' in message and 'bus' in message) or 185 | 'command_response' in message): 186 | return False 187 | return True 188 | 189 | def parse_messages(self): 190 | while True: 191 | message = self.streamer.parse_next_message() 192 | if message is None: 193 | break 194 | 195 | if not self._message_valid(message): 196 | self.corrupted_messages += 1 197 | break 198 | 199 | if self.callback is not None: 200 | self.callback(message) 201 | self._receive_command_response(message) 202 | 203 | def run(self): 204 | """Continuously read data from the source and attempt to parse a valid 205 | message from the buffer of bytes. When a message is parsed, passes it 206 | off to the callback if one is set. 207 | """ 208 | while self.running: 209 | payload = "" 210 | payloadsave = "" 211 | try: 212 | payload = self.read() 213 | try: 214 | payloadsave = str(payload, "cp437", "ignore") 215 | except: 216 | payloadsave = "" 217 | except DataSourceError as e: 218 | if self.running: 219 | LOG.warn("Can't read from data source -- stopping: %s", e) 220 | break 221 | 222 | try: 223 | self.streamer 224 | except MissingPayloadFormatError: 225 | json_chars = ['\x00'] 226 | json_chars.extend(string.printable) 227 | if all((char in json_chars for char in payloadsave)): 228 | self.format = "json" 229 | else: 230 | self.format = "protobuf" 231 | self.streamer.receive(payload) 232 | self.parse_messages() 233 | 234 | def _receive_command_response(self, message): 235 | # TODO the controller/source are getting a little mixed up since the 236 | # controller now needs to receive responses from the soruce side, maybe 237 | # just mix them again. the only exception to being both is a trace 238 | # source, and we can just leave the controller methods on that 239 | # unimplemented 240 | self.open_requests = getattr(self, 'open_requests', []) 241 | for open_request in self.open_requests: 242 | open_request.put(message) 243 | 244 | 245 | class DataSourceError(Exception): 246 | pass 247 | -------------------------------------------------------------------------------- /openxc/sources/bluetooth.py: -------------------------------------------------------------------------------- 1 | """A Bluetooth data source.""" 2 | 3 | 4 | import logging 5 | 6 | from openxc.controllers.base import Controller 7 | from .socket import SocketDataSource 8 | from .base import DataSourceError 9 | 10 | LOG = logging.getLogger(__name__) 11 | 12 | try: 13 | import bluetooth 14 | except ImportError: 15 | LOG.debug("pybluez library not installed, can't use bluetooth interface") 16 | bluetooth = None 17 | 18 | 19 | class BluetoothVehicleInterface(SocketDataSource, Controller): 20 | """A data source reading from a bluetooth device. 21 | """ 22 | 23 | OPENXC_DEVICE_NAME_PREFIX = "OpenXC-VI-" 24 | 25 | def __init__(self, address=None, **kwargs): 26 | """Initialize a connection to the bluetooth device. 27 | 28 | Raises: 29 | DataSourceError if the bluetooth device cannot be opened. 30 | """ 31 | super(BluetoothVehicleInterface, self).__init__(**kwargs) 32 | self.address = address 33 | 34 | if bluetooth is None: 35 | raise DataSourceError("pybluez library is not available") 36 | 37 | while self.address is None: 38 | self.scan_for_bluetooth_device() 39 | self.connect() 40 | 41 | def connect(self): 42 | # TODO push this to a background connecting thread so the constructor 43 | # can return 44 | port = 1 45 | connected = False 46 | while not connected: 47 | self.socket = bluetooth.BluetoothSocket(bluetooth.RFCOMM) 48 | try: 49 | self.socket.connect((self.address, port)) 50 | except IOError as e: 51 | LOG.warn("Unable to connect to %s" % self.address, e) 52 | else: 53 | LOG.info("Opened bluetooth device at %s", port) 54 | connected = True 55 | 56 | def scan_for_bluetooth_device(self): 57 | nearby_devices = bluetooth.discover_devices() 58 | 59 | self.address = None 60 | device_name = None 61 | for address in nearby_devices: 62 | device_name = bluetooth.lookup_name(address) 63 | if (device_name is not None and 64 | device_name.startswith(self.OPENXC_DEVICE_NAME_PREFIX)): 65 | self.address = address 66 | break 67 | 68 | if self.address is not None: 69 | LOG.info("Discovered OpenXC VI %s (%s)" % (device_name, self.address)) 70 | else: 71 | LOG.info("No OpenXC VI devices discovered") 72 | -------------------------------------------------------------------------------- /openxc/sources/network.py: -------------------------------------------------------------------------------- 1 | """A network socket data source.""" 2 | 3 | 4 | import logging 5 | import socket 6 | 7 | from .base import DataSourceError 8 | from .socket import SocketDataSource 9 | 10 | LOG = logging.getLogger(__name__) 11 | 12 | 13 | class NetworkDataSource(SocketDataSource): 14 | """A data source reading from a network socket, as implemented 15 | in the openxc-vehicle-simulator . 16 | """ 17 | DEFAULT_PORT = 50001 18 | 19 | def __init__(self, host=None, port=None, **kwargs): 20 | """Initialize a connection to the network socket. 21 | 22 | Kwargs: 23 | host - optionally override the default network host (default is local machine) 24 | port - optionally override the default network port (default is 50001) 25 | log_mode - optionally record or print logs from the network source 26 | 27 | Raises: 28 | DataSourceError if the socket connection cannot be opened. 29 | """ 30 | super(NetworkDataSource, self).__init__(**kwargs) 31 | self.host = host or socket.gethostbyname(socket.gethostname()) 32 | self.port = port or self.DEFAULT_PORT 33 | self.port = int(self.port) 34 | 35 | try: 36 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 37 | self.socket.connect((self.host, self.port)) 38 | except (OSError, socket.error) as e: 39 | raise DataSourceError("Unable to open socket connection at " 40 | "%s:%s: %s" % (self.host,self.port, e)) 41 | else: 42 | LOG.debug("Opened socket connection at %s:%s", self.host, self.port) 43 | -------------------------------------------------------------------------------- /openxc/sources/serial.py: -------------------------------------------------------------------------------- 1 | """A virtual serial port data source.""" 2 | 3 | 4 | import logging 5 | 6 | from .base import BytestreamDataSource, DataSourceError 7 | 8 | LOG = logging.getLogger(__name__) 9 | 10 | try: 11 | import serial 12 | except ImportError: 13 | LOG.debug("serial library not installed, can't use serial interface") 14 | serial = None 15 | 16 | 17 | class SerialDataSource(BytestreamDataSource): 18 | """A data source reading from a serial port, which could be implemented 19 | with a USB to Serial or Bluetooth adapter. 20 | """ 21 | DEFAULT_PORT = "/dev/ttyUSB0" 22 | DEFAULT_BAUDRATE = 230400 23 | 24 | def __init__(self, port=None, baudrate=None, **kwargs): 25 | """Initialize a connection to the serial device. 26 | 27 | Kwargs: 28 | port - optionally override the default virtual COM port 29 | baudrate - optionally override the default baudrate 30 | 31 | Raises: 32 | DataSourceError if the serial device cannot be opened. 33 | """ 34 | super(SerialDataSource, self).__init__(**kwargs) 35 | port = port or self.DEFAULT_PORT 36 | baudrate = baudrate or self.DEFAULT_BAUDRATE 37 | 38 | if serial is None: 39 | raise DataSourceError("pyserial library is not available") 40 | 41 | try: 42 | self.device = serial.Serial(port, baudrate, rtscts=True) 43 | except (OSError, serial.SerialException) as e: 44 | raise DataSourceError("Unable to open serial device at port " 45 | "%s: %s" % (port, e)) 46 | else: 47 | LOG.debug("Opened serial device at %s", port) 48 | 49 | def read(self): 50 | return self.device.read() 51 | -------------------------------------------------------------------------------- /openxc/sources/socket.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from .base import BytestreamDataSource, DataSourceError 4 | 5 | # TODO can we get rid of this? 6 | import socket 7 | 8 | 9 | class SocketDataSource(BytestreamDataSource): 10 | def read(self): 11 | try: 12 | line = "" 13 | while '\x00' not in line: 14 | # TODO this is fairly inefficient 15 | line += self.socket.recv(1) 16 | except (OSError, socket.error, IOError) as e: 17 | raise DataSourceError("Unable to read from socket connection at " 18 | "%s:%s: %s" % (self.host,self.port, e)) 19 | if not line: 20 | raise DataSourceError("Unable to read from socket connection at " 21 | "%s:%s" % (self.host,self.port)) 22 | return line 23 | 24 | def write_bytes(self, data): 25 | return self.socket.send(data) 26 | -------------------------------------------------------------------------------- /openxc/sources/trace.py: -------------------------------------------------------------------------------- 1 | """A data source for reading from pre-recorded OpenXC trace files.""" 2 | 3 | 4 | 5 | import logging 6 | import time 7 | 8 | from .base import DataSourceError, BytestreamDataSource 9 | 10 | from openxc.formats.json import JsonFormatter 11 | 12 | LOG = logging.getLogger(__name__) 13 | 14 | class TraceDataSource(BytestreamDataSource): 15 | """A class to replay a previously recorded OpenXC vehicle data trace file. 16 | For details on the trace file format, see 17 | http://openxcplatform.com/android/testing.html. 18 | """ 19 | 20 | def __init__(self, filename=None, realtime=True, loop=True, **kwargs): 21 | """Construct the source and attempt to open the trace file. 22 | 23 | filename - the full absolute path to the trace file 24 | 25 | realtime - if ``True``, the trace will be replayed at approximately 26 | the same cadence as it was recorded. Otherwise, the trace file 27 | will be replayed as fast as possible (likely much faster than 28 | any vehicle). 29 | 30 | loop - if ``True``, the trace file will be looped and will provide 31 | data until the process exist or the source is stopped. 32 | """ 33 | super(TraceDataSource, self).__init__(**kwargs) 34 | self.realtime = realtime 35 | self.loop = loop 36 | self.filename = filename 37 | self._reopen_file() 38 | 39 | def _reopen_file(self): 40 | if getattr(self, 'trace_file', None) is not None: 41 | self.trace_file.close() 42 | self.trace_file = self._open_file(self.filename) 43 | self.starting_time = time.time() 44 | 45 | def _store_timestamp(self, timestamp): 46 | """If not already saved, cache the first timestamp in the active trace 47 | file on the instance. 48 | """ 49 | if getattr(self, 'first_timestamp', None) is None: 50 | self.first_timestamp = timestamp 51 | LOG.debug("Storing %d as the first timestamp of the trace file %s", 52 | self.first_timestamp, self.filename) 53 | 54 | def read(self): 55 | """Read a line of data from the input source at a time.""" 56 | line = self.trace_file.readline() 57 | if line == '': 58 | if self.loop: 59 | self._reopen_file() 60 | else: 61 | self.trace_file.close() 62 | self.trace_file = None 63 | raise DataSourceError() 64 | 65 | message = JsonFormatter.deserialize(line) 66 | timestamp = message.get('timestamp', None) 67 | if self.realtime and timestamp is not None: 68 | self._store_timestamp(timestamp) 69 | self._wait(self.starting_time, self.first_timestamp, timestamp) 70 | return (line + "\x00").encode("cp437") 71 | 72 | @staticmethod 73 | def _open_file(filename): 74 | """Attempt to open the the file at ``filename`` for reading. 75 | 76 | Raises: 77 | DataSourceError, if the file cannot be opened. 78 | """ 79 | if filename is None: 80 | raise DataSourceError("Trace filename is not defined") 81 | 82 | try: 83 | trace_file = open(filename, "r") 84 | except IOError as e: 85 | raise DataSourceError("Unable to open trace file %s" % filename, e) 86 | else: 87 | LOG.debug("Opened trace file %s", filename) 88 | return trace_file 89 | 90 | @staticmethod 91 | def _wait(starting_time, first_timestamp, timestamp): 92 | """Given that the first timestamp in the trace file is 93 | ``first_timestamp`` and we started playing back the file at 94 | ``starting_time``, block until the current ``timestamp`` should occur. 95 | """ 96 | target_time = starting_time + (timestamp - first_timestamp) 97 | time.sleep(max(target_time - time.time(), 0)) 98 | -------------------------------------------------------------------------------- /openxc/sources/usb.py: -------------------------------------------------------------------------------- 1 | """A USB vehicle interface data source.""" 2 | 3 | import os 4 | import logging 5 | import usb.core 6 | import usb.util 7 | from usb.backend import libusb0 8 | 9 | from .base import BytestreamDataSource, DataSourceError 10 | 11 | LOG = logging.getLogger(__name__) 12 | 13 | 14 | class UsbDataSource(BytestreamDataSource): 15 | """A source to receive data from an OpenXC vehicle interface via USB.""" 16 | DEFAULT_VENDOR_ID = 0x1bc4 17 | DEFAULT_PRODUCT_ID = 0x0001 18 | DEFAULT_READ_REQUEST_SIZE = 512 19 | 20 | # If we don't get DEFAULT_READ_REQUEST_SIZE bytes within this number of 21 | # milliseconds, bail early and return whatever we have - could be zero, 22 | # could be just less than 512. If data is really pumpin' we can get better 23 | # throughput if the READ_REQUEST_SIZE is higher, but this delay has to be 24 | # low enough that a single request isn't held back too long. 25 | DEFAULT_READ_TIMEOUT = 200 26 | LIBUSB0_TIMEOUT_CODE = -116 27 | LIBUSB1_TIMEOUT_CODE = -7 28 | OPENUSB_TIMEOUT_CODE = -62 29 | 30 | DEFAULT_INTERFACE_NUMBER = 0 31 | VEHICLE_DATA_IN_ENDPOINT = 2 32 | LOG_IN_ENDPOINT = 11 33 | 34 | def __init__(self, vendor_id=None, product_id=None, **kwargs): 35 | """Initialize a connection to the USB device's IN endpoint. 36 | 37 | Kwargs: 38 | vendor_id (str or int) - optionally override the USB device vendor 39 | ID we will attempt to connect to, if not using the OpenXC 40 | hardware. 41 | 42 | product_id (str or int) - optionally override the USB device product 43 | ID we will attempt to connect to, if not using the OpenXC 44 | hardware. 45 | 46 | log_mode - optionally record or print logs from the USB device, which 47 | are on a separate channel. 48 | 49 | Raises: 50 | DataSourceError if the USB device with the given vendor ID is not 51 | connected. 52 | """ 53 | super(UsbDataSource, self).__init__(**kwargs) 54 | if vendor_id is not None and not isinstance(vendor_id, int): 55 | vendor_id = int(vendor_id, 0) 56 | self.vendor_id = vendor_id or self.DEFAULT_VENDOR_ID 57 | 58 | if product_id is not None and not isinstance(product_id, int): 59 | product_id = int(product_id, 0) 60 | self.product_id = product_id or self.DEFAULT_PRODUCT_ID 61 | 62 | if os.name == 'nt': 63 | be = libusb0.get_backend() 64 | devices = usb.core.find(backend=be, find_all=True, idVendor=self.vendor_id, 65 | idProduct=self.product_id) 66 | else: 67 | devices = usb.core.find(find_all=True, idVendor=self.vendor_id, 68 | idProduct=self.product_id) 69 | 70 | for device in devices: 71 | self.device = device 72 | try: 73 | self.device.set_configuration() 74 | except usb.core.USBError as e: 75 | LOG.warn("Skipping USB device: %s", e) 76 | else: 77 | return 78 | 79 | raise DataSourceError("No USB vehicle interface detected - is one plugged in?") 80 | 81 | def read(self, timeout=None): 82 | return self._read(self.VEHICLE_DATA_IN_ENDPOINT, timeout) 83 | 84 | def read_logs(self, timeout=None): 85 | return self._read(self.LOG_IN_ENDPOINT, timeout, 64) 86 | 87 | def stop(self): 88 | super(UsbDataSource, self).stop() 89 | usb.util.dispose_resources(self.device) 90 | 91 | def _read(self, endpoint_address, timeout=None, 92 | read_size=DEFAULT_READ_REQUEST_SIZE): 93 | timeout = timeout or self.DEFAULT_READ_TIMEOUT 94 | try: 95 | raw_binary = self.device.read(0x80 + endpoint_address,read_size, self.DEFAULT_INTERFACE_NUMBER, timeout) 96 | return raw_binary 97 | 98 | except (usb.core.USBError, AttributeError) as e: 99 | if e.backend_error_code in [self.LIBUSB0_TIMEOUT_CODE, self.LIBUSB1_TIMEOUT_CODE, self.OPENUSB_TIMEOUT_CODE]: 100 | # Timeout, it may just not be sending 101 | return "" 102 | raise DataSourceError("USB device couldn't be read", e) 103 | -------------------------------------------------------------------------------- /openxc/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openxc/openxc-python/5341180fea6c364027dedc9bc4c8027b2831325f/openxc/tools/__init__.py -------------------------------------------------------------------------------- /openxc/tools/args.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openxc/openxc-python/5341180fea6c364027dedc9bc4c8027b2831325f/openxc/tools/args.py -------------------------------------------------------------------------------- /openxc/tools/common.py: -------------------------------------------------------------------------------- 1 | """Common functions for parsing command line arguments, shared with all openxc 2 | command line utilities. 3 | """ 4 | import argparse 5 | import logging 6 | 7 | from openxc.sources.trace import TraceDataSource 8 | from openxc.interface import SerialVehicleInterface, UsbVehicleInterface, \ 9 | BluetoothVehicleInterface 10 | 11 | 12 | def device_options(): 13 | parser = argparse.ArgumentParser(add_help=False) 14 | device_group = parser.add_mutually_exclusive_group() 15 | device_group.add_argument("--usb", action="store_true", dest="use_usb", 16 | default=True, 17 | help="use a USB-connected VI as the data source") 18 | device_group.add_argument("--serial", action="store_true", 19 | dest="use_serial", 20 | help="use a serial-connected VI as the data source") 21 | device_group.add_argument("--bluetooth", action="store_true", 22 | dest="use_bluetooth", 23 | help="use a Bluetooth-connected VI as the data source") 24 | device_group.add_argument("--network", action="store_true", 25 | dest="use_network", 26 | help="use a network-connected VI as the data source") 27 | device_group.add_argument("--trace", action="store", dest="trace_file", 28 | help="use a pre-recorded OpenXC JSON trace file as the data source") 29 | parser.add_argument("--usb-vendor", 30 | action="store", 31 | dest="usb_vendor", 32 | help="USB vendor ID for the VI") 33 | parser.add_argument("--usb-product", 34 | action="store", 35 | dest="usb_product", 36 | help="USB product ID for the VI") 37 | parser.add_argument("--bluetooth-address", 38 | action="store", 39 | dest="bluetooth_address", 40 | help="MAC address of Bluetooth VI. If not provided, will " + 41 | "perform a scan and select first device with name " + 42 | "matching \"OpenXC-VI-*\"") 43 | parser.add_argument("--serial-port", 44 | action="store", 45 | dest="serial_port", 46 | help="virtual COM port path for serial VI") 47 | parser.add_argument("--serial-baudrate", 48 | action="store", 49 | dest="baudrate", 50 | help="baudrate for serial-connected VI") 51 | parser.add_argument("--log-mode", 52 | action="store", 53 | default="off", 54 | choices=["off", "stderr", "file"], 55 | dest="log_mode", 56 | help="record logs to a file or stderr, if available from the interface") 57 | parser.add_argument("--format", 58 | action="store", 59 | choices=["json", "protobuf"], 60 | dest="format", 61 | help="select the data format for sending and receiving with the VI") 62 | return parser 63 | 64 | 65 | def configure_logging(level=logging.WARN): 66 | logging.getLogger("openxc").addHandler(logging.StreamHandler()) 67 | logging.getLogger("openxc").setLevel(level) 68 | 69 | def select_device(arguments): 70 | if arguments.use_serial: 71 | source_class = SerialVehicleInterface 72 | source_kwargs = dict(port=arguments.serial_port, 73 | baudrate=arguments.baudrate) 74 | elif arguments.trace_file: 75 | source_class = TraceDataSource 76 | source_kwargs = dict(filename=arguments.trace_file) 77 | elif arguments.use_bluetooth: 78 | source_class = BluetoothVehicleInterface 79 | source_kwargs = dict(address=arguments.bluetooth_address) 80 | else: 81 | source_class = UsbVehicleInterface 82 | source_kwargs = dict(vendor_id=arguments.usb_vendor, 83 | product_id=arguments.usb_product) 84 | 85 | source_kwargs['log_mode'] = arguments.log_mode 86 | source_kwargs['payload_format'] = arguments.format 87 | return source_class, source_kwargs 88 | -------------------------------------------------------------------------------- /openxc/tools/control.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the methods for the ``openxc-control`` command line 3 | program. 4 | 5 | `main` is executed when ``openxc-control`` is run, and all other callables in 6 | this module are internal only. 7 | """ 8 | 9 | 10 | import argparse 11 | import sys 12 | import time 13 | 14 | from openxc.formats.json import JsonFormatter 15 | from .common import device_options, configure_logging, select_device 16 | 17 | 18 | def version(interface): 19 | print(("Device is running version %s" % interface.version())) 20 | 21 | def platform(interface): 22 | print(("Device is a %s" % interface.platform())) 23 | 24 | def sd_mount_status(interface): 25 | result = interface.sd_mount_status() 26 | if(result == 1): 27 | print("SD card mount status: true") 28 | else: 29 | print("SD card mount status: false") 30 | 31 | def device_id(interface): 32 | print(("Device ID is %s" % interface.device_id())) 33 | 34 | def get_vin(interface): 35 | print(("Vehicle VIN is %s" % interface.get_vin())) 36 | 37 | def passthrough(interface, bus, passthrough_enabled): 38 | if interface.set_passthrough(bus, passthrough_enabled): 39 | print(("Bus %u passthrough set to %s" % (bus, passthrough_enabled))) 40 | 41 | def af_bypass(interface, bus, bypass): 42 | if interface.set_acceptance_filter_bypass(bus, bypass): 43 | if bypass: 44 | bypass_string = "bypassed" 45 | else: 46 | bypass_string = "enabled" 47 | print(("Bus %u AF is now %s" % (bus, bypass_string))) 48 | 49 | def set_payload_format(interface, payload_format): 50 | if interface.set_payload_format(payload_format): 51 | print(("Changed payload format to %s" % payload_format)) 52 | 53 | def set_rtc_time(interface, unix_time): 54 | if interface.rtc_configuration(unix_time): 55 | print(("Time set to %d" % unix_time)) 56 | 57 | def modem_configuration(interface, host, port): 58 | if interface.modem_configuration(host, port): 59 | print(("host set to %s:%s" %(host, port))) 60 | 61 | def write_file(interface, filename): 62 | first_timestamp = None 63 | with open(filename, "r") as output_file: 64 | corrupt_entries = 0 65 | message_count = 0 66 | start_time = time.time() 67 | for line in output_file: 68 | try: 69 | parsed_message = JsonFormatter.deserialize(line.encode("utf-8")) 70 | if not isinstance(parsed_message, dict): 71 | raise ValueError() 72 | except ValueError: 73 | corrupt_entries += 1 74 | else: 75 | # TODO at the moment it's taking longer to write all of 76 | # individual CAN messages than the time that actually 77 | # elapsed in receiving the trace - need to implement 78 | # batching to speed this up. right now this will never sleep 79 | # because it's always behind. 80 | timestamp = parsed_message.get('timestamp', None) 81 | # TODO this duplicates some code from sources/trace.py 82 | if timestamp is not None: 83 | first_timestamp = first_timestamp or timestamp 84 | target_time = start_time + (timestamp - first_timestamp) 85 | time.sleep(max(.0002, target_time - time.time())) 86 | 87 | message_count += 1 88 | interface.write(**parsed_message) 89 | print(("%d lines sent" % message_count)) 90 | if corrupt_entries > 0: 91 | print(("%d invalid lines in the data file were not sent" % 92 | corrupt_entries)) 93 | 94 | 95 | def parse_options(): 96 | parser = argparse.ArgumentParser(description="Send control messages to an " 97 | "attached OpenXC vehicle interface", parents=[device_options()]) 98 | parser.add_argument("command", type=str, 99 | choices=['version', 'platform', 'write', 'id', 'set', 'sd_mount_status', 'get_vin']) 100 | write_group = parser.add_mutually_exclusive_group() 101 | write_group.add_argument("--name", action="store", dest="write_name", 102 | help="name for message write request") 103 | write_group.add_argument("--id", action="store", dest="write_id", 104 | help="ID for raw message write request") 105 | parser.add_argument("--bus", action="store", dest="bus", 106 | default=1, 107 | help="CAN bus number for the control request") 108 | parser.add_argument("--value", action="store", dest="write_value", 109 | help="optional value for message write request") 110 | parser.add_argument("--event", action="store", dest="write_event", 111 | help="optional event for message write request") 112 | parser.add_argument("--data", action="store", dest="write_data", 113 | help="data for raw message write request") 114 | parser.add_argument("--frame-format", action="store", 115 | dest="write_frame_format", choices=['standard', 'extended'], 116 | help="explicit frame format for raw message write request") 117 | write_group.add_argument("-f", "--write-input-file", action="store", 118 | dest="write_input_file", 119 | help="the path to a file with a list of raw or translated " 120 | "messages to write to the selected vehicle interface") 121 | parser.add_argument("--passthrough", action="store_true", default=None, 122 | dest="passthrough_enabled") 123 | parser.add_argument("--no-passthrough", action="store_false", default=None, 124 | dest="passthrough_enabled") 125 | parser.add_argument("--af-bypass", action="store_true", default=None, 126 | dest="af_bypass") 127 | parser.add_argument("--no-af-bypass", action="store_false", default=None, 128 | dest="af_bypass") 129 | parser.add_argument("--new-payload-format", action="store", default=None, 130 | choices=['json', 'protobuf'], dest="new_payload_format") 131 | parser.add_argument("--time", action="store",default=None, 132 | dest="unix_time") 133 | parser.add_argument("--host", action="store", default=None, 134 | dest="host") 135 | parser.add_argument("--network-host", action="store", default=None, 136 | dest="network_host") 137 | parser.add_argument("--port", action="store", default=80, 138 | dest="port") 139 | parser.set_defaults(format="json") 140 | return parser.parse_args() 141 | 142 | def handle_set_command(arguments, interface): 143 | if arguments.passthrough_enabled is not None: 144 | passthrough(interface, int(arguments.bus), arguments.passthrough_enabled) 145 | if arguments.af_bypass is not None: 146 | af_bypass(interface, int(arguments.bus), arguments.af_bypass) 147 | if arguments.new_payload_format is not None: 148 | set_payload_format(interface, arguments.new_payload_format) 149 | if arguments.unix_time is not None: 150 | set_rtc_time(interface, int(arguments.unix_time)) 151 | if arguments.network_host is not None: 152 | modem_configuration(interface, arguments.network_host, arguments.port) 153 | 154 | def handle_write_command(arguments, interface): 155 | if arguments.write_name: 156 | interface.write(name=arguments.write_name, 157 | value=arguments.write_value, 158 | event=arguments.write_event) 159 | elif arguments.write_id: 160 | if not arguments.write_data: 161 | sys.exit("%s requires an id and data" % arguments.command) 162 | # TODO we should use unhexlify as with the diagnostic command 163 | # payloads so we can standardize the API and not deal with hex 164 | # strings in code 165 | interface.write(bus=int(arguments.bus), 166 | id=arguments.write_id, 167 | data=arguments.write_data, 168 | frame_format=arguments.write_frame_format) 169 | elif arguments.write_input_file: 170 | write_file(interface, arguments.write_input_file) 171 | else: 172 | sys.exit("%s requires a signal name, message ID or filename" % arguments.command) 173 | 174 | def main(): 175 | configure_logging() 176 | arguments = parse_options() 177 | 178 | interface_class, interface_kwargs = select_device(arguments) 179 | interface = interface_class(**interface_kwargs) 180 | interface.start() 181 | 182 | handle_set_command(arguments, interface) 183 | 184 | if arguments.command == "version": 185 | version(interface) 186 | elif arguments.command == "platform": 187 | platform(interface) 188 | elif arguments.command == "sd_mount_status": 189 | sd_mount_status(interface) 190 | elif arguments.command == "id": 191 | device_id(interface) 192 | elif arguments.command == "get_vin": 193 | get_vin(interface) 194 | elif arguments.command == "set": 195 | handle_set_command(arguments, interface) 196 | elif arguments.command == "write": 197 | handle_write_command(arguments, interface) 198 | else: 199 | print(("Unrecognized command \"%s\"" % arguments.command)) -------------------------------------------------------------------------------- /openxc/tools/dashboard.py: -------------------------------------------------------------------------------- 1 | """ This module contains the methods for the ``openxc-dashboard`` command line 2 | program. 3 | 4 | `main` is executed when ``openxc-dashboard`` is run, and all other callables in 5 | this module are internal only. 6 | """ 7 | 8 | import argparse 9 | import threading 10 | import logging 11 | from flask import Flask 12 | from flask import render_template 13 | from flask_socketio import SocketIO 14 | 15 | from openxc.interface import UsbVehicleInterface 16 | 17 | log = logging.getLogger('werkzeug') 18 | log.setLevel(logging.ERROR) 19 | app = Flask(__name__) 20 | socketio = SocketIO(app) 21 | 22 | def vehicle_data_thread(): 23 | vi = UsbVehicleInterface(callback=send_data) 24 | vi.start() 25 | 26 | def send_data(data, **kwargs): 27 | socketio.emit('vehicle data', data, broadcast=True) 28 | 29 | @app.route("/") 30 | def dashboard_static(): 31 | try: 32 | vehicle_data_thread() 33 | except: 34 | pass 35 | return render_template('dashboard.html') 36 | 37 | def parse_options(): 38 | parser = argparse.ArgumentParser( 39 | description="View a real-time dashboard of all OpenXC measurements", 40 | parents=[device_options()]) 41 | arguments = parser.parse_args() 42 | return arguments 43 | 44 | 45 | def main(): 46 | socketio.start_background_task(vehicle_data_thread) 47 | print("View the dashboard at http://127.0.0.1:5000") 48 | app.run() 49 | -------------------------------------------------------------------------------- /openxc/tools/diagnostics.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the methods for the ``openxc-diag`` command line 3 | program. 4 | 5 | `main` is executed when ``openxc-diag`` is run, and all other callables in 6 | this module are internal only. 7 | """ 8 | 9 | 10 | import binascii 11 | import argparse 12 | import time 13 | 14 | from .common import device_options, configure_logging, select_device 15 | 16 | def print_diagnostic_response(responses): 17 | for response in responses: 18 | # After sending a diagnostic request, it will return with a signal message saying if the 19 | # request was recieved. After that it used to show about 30 vehicle messages (rpm, speed, etc) 20 | # with the actual diagnostic response mixed in. So, if the response length is more than 21 | # 1, it's the response, if its less (only 1) it's the recieved message. 22 | if len(response) > 1: 23 | # Stripping all of the unnesseary data we get after sending a diag request in python 24 | # Just like in enabler, it's a diag response if it contains the keys "mode", "bus", 25 | # "id", and "success". 26 | diag_mgs_req_keys = ['mode', 'bus', 'id', 'success'] 27 | indices = [i for i, s in enumerate(response) if all(x in s for x in diag_mgs_req_keys)] 28 | if indices: 29 | print(("Response: %s" % response[indices[0]])) 30 | else: 31 | print(("Response: %s" % response)) 32 | 33 | def diagnostic_request(arguments, controller): 34 | message = int(arguments.message, 0) 35 | mode = int(arguments.mode, 0) 36 | 37 | bus = None 38 | if arguments.bus is not None: 39 | bus = int(arguments.bus, 0) 40 | 41 | pid = None 42 | if arguments.pid is not None: 43 | pid = int(arguments.pid, 0) 44 | 45 | frequency = None 46 | if arguments.frequency is not None: 47 | frequency = int(arguments.frequency, 0) 48 | 49 | payload = bytearray() 50 | if arguments.payload is not None: 51 | payload = binascii.unhexlify(arguments.payload.split("0x")[1]) 52 | 53 | if arguments.command == "add": 54 | responses = controller.create_diagnostic_request(message, mode, bus=bus, 55 | pid=pid, frequency=frequency, payload=payload, 56 | wait_for_first_response=True) 57 | if len(responses) == 0: 58 | print("No response received before timeout") 59 | else: 60 | print_diagnostic_response(responses) 61 | elif arguments.command == "cancel": 62 | if controller.delete_diagnostic_request(message, mode, bus=bus, 63 | pid=pid): 64 | print("Diagnostic request deleted successfully") 65 | else: 66 | print("Error when attempting delete") 67 | 68 | 69 | def parse_options(): 70 | parser = argparse.ArgumentParser( 71 | description="Sends a diagnostic message request to a vehicle interface", 72 | parents=[device_options()]) 73 | parser.add_argument("command", type=str, choices=['add', 'cancel']) 74 | # TODO need to be able to specify name, factor, offset. Needs to be 75 | # supported in the controller, too. 76 | parser.add_argument("--message", required=True, help="CAN message ID for the request") 77 | parser.add_argument("--mode", required=True, help="Diagnostic mode (or service) number") 78 | parser.add_argument("--bus", help="CAN bus controller address to send on") 79 | parser.add_argument("--pid", help="Parameter ID (e.g. for Mode 1 request") 80 | parser.add_argument("--payload", help="A byte array as a hex string to send as payload, e.g. 0x123") 81 | parser.add_argument("--frequency", help="Frequency (Hz) to repeat this request. If omitted or 0, it will be a one-time request.") 82 | parser.set_defaults(format="json") 83 | 84 | return parser.parse_args() 85 | 86 | 87 | def main(): 88 | configure_logging() 89 | arguments = parse_options() 90 | 91 | controller_class, controller_kwargs = select_device(arguments) 92 | controller = controller_class(**controller_kwargs) 93 | controller.start() 94 | 95 | diagnostic_request(arguments, controller) 96 | controller.stop() 97 | -------------------------------------------------------------------------------- /openxc/tools/dump.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the methods for the ``openxc-dump`` command line program. 3 | 4 | `main` is executed when ``openxc-dump`` is run, and all other callables in this 5 | module are internal only. 6 | """ 7 | 8 | 9 | import argparse 10 | import time 11 | import logging 12 | import sys 13 | 14 | from openxc.formats.json import JsonFormatter 15 | from .common import device_options, configure_logging, select_device 16 | 17 | import functools 18 | print = functools.partial(print, flush=True) 19 | 20 | def receive(message, **kwargs): 21 | message['timestamp'] = time.time() 22 | print((JsonFormatter.serialize(message))) 23 | 24 | 25 | def parse_options(): 26 | parser = argparse.ArgumentParser( 27 | description="View a raw OpenXC data stream", 28 | parents=[device_options()]) 29 | 30 | arguments = parser.parse_args() 31 | return arguments 32 | 33 | 34 | def main(): 35 | try: 36 | configure_logging(logging.DEBUG) 37 | arguments = parse_options() 38 | source_class, source_kwargs = select_device(arguments) 39 | source = source_class(callback=receive, **source_kwargs) 40 | source.start() 41 | # TODO test this, I'd prefer it to the sleep loop 42 | while(True): 43 | source.join(0.1) 44 | except KeyboardInterrupt: 45 | sys.exit(0) 46 | -------------------------------------------------------------------------------- /openxc/tools/generate_code.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | This module is the main entry point for generating vehicle interface source code 4 | from JSON mappings. 5 | 6 | Given one or more JSON mapping filenames, and a list of directories where 7 | configuration files may be found, it parses the mappings, validates them, and 8 | prints the resulting source to stdout. 9 | """ 10 | 11 | 12 | import sys 13 | import argparse 14 | import logging 15 | 16 | from openxc.generator.coder import CodeGenerator 17 | from openxc.generator.message_sets import JsonMessageSet 18 | from openxc.utils import fatal_error, load_json_from_search_path 19 | from .common import configure_logging 20 | 21 | LOG = logging.getLogger(__name__) 22 | 23 | DEFAULT_SEARCH_PATH = "." 24 | 25 | 26 | def parse_options(): 27 | parser = argparse.ArgumentParser(description="Generate C++ source code " 28 | "from CAN signal descriptions in JSON") 29 | 30 | group = parser.add_mutually_exclusive_group(required=True) 31 | group.add_argument("-m", "--message-sets", 32 | type=str, 33 | nargs='+', 34 | dest="message_sets", 35 | metavar="MESSAGE_SET", 36 | help="generate source from these JSON-formatted message " 37 | "set definitions") 38 | group.add_argument("--super-set", 39 | type=str, 40 | dest="super_set", 41 | metavar="SUPERSET", 42 | help="generate source with multiple active message sets, defined in" 43 | " this JSON-formatted superset") 44 | parser.add_argument("-s", "--search-paths", 45 | type=str, 46 | nargs='+', 47 | dest="search_paths", 48 | metavar="PATH", 49 | help="add directories to the search path when using relative paths") 50 | 51 | return parser.parse_args() 52 | 53 | 54 | def main(): 55 | configure_logging() 56 | arguments = parse_options() 57 | 58 | search_paths = arguments.search_paths or [] 59 | search_paths.insert(0, DEFAULT_SEARCH_PATH) 60 | 61 | message_sets = arguments.message_sets or [] 62 | if arguments.super_set is not None: 63 | super_set_data = load_json_from_search_path(arguments.super_set, 64 | arguments.search_paths) 65 | super_set_message_sets = super_set_data.get('message_sets', []) 66 | if len(super_set_message_sets) == 0: 67 | LOG.warning("Superset '%s' has no message sets" % 68 | super_set_data.get('name', 'unknown')) 69 | message_sets.extend(super_set_message_sets) 70 | 71 | generator = CodeGenerator(search_paths) 72 | for filename in message_sets: 73 | message_set = JsonMessageSet.parse(filename, search_paths=search_paths, 74 | skip_disabled_mappings=True) 75 | if not message_set.validate_messages() or not message_set.validate_name(): 76 | fatal_error("unable to generate code") 77 | generator.message_sets.append(message_set) 78 | 79 | # TODO dump to a filename or pipe instead 80 | print(generator.build_source()) 81 | 82 | 83 | if __name__ == "__main__": 84 | sys.exit(main()) 85 | -------------------------------------------------------------------------------- /openxc/tools/gps.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the methods for the ``openxc-gps`` command line program. 3 | 4 | `main` is executed when ``openxc-gps`` is run, and all other callables in this 5 | module are internal only. 6 | """ 7 | 8 | 9 | import argparse 10 | from xml.etree import ElementTree as ET 11 | 12 | from openxc.sources.trace import TraceDataSource 13 | from .common import device_options, configure_logging, select_device 14 | 15 | 16 | class GPXTranscoder(object): 17 | def __init__(self): 18 | self.root = ET.Element("gpx") 19 | track = ET.SubElement(self.root, "trk") 20 | number = ET.SubElement(track, "number") 21 | number.text = "1" 22 | self.segment = ET.SubElement(track, "trkseg") 23 | self.latitude = self.longitude = None 24 | 25 | def output(self): 26 | return ET.tostring(ET.ElementTree(self.root).getroot()) 27 | 28 | def receive(self, message, **kwargs): 29 | if message['name'] == 'latitude': 30 | self.latitude = message['value'] 31 | elif message['name'] == 'longitude': 32 | self.longitude = message['value'] 33 | 34 | if self.latitude and self.longitude: 35 | point = ET.SubElement(self.segment, "trkpt") 36 | point.set('lat', str(self.latitude)) 37 | point.set('lon', str(self.longitude)) 38 | self.latitude = self.longitude = None 39 | 40 | 41 | def parse_options(): 42 | parser = argparse.ArgumentParser(description= 43 | "Create a GPS trace in various formats from OpenXC input data") 44 | parser.add_argument("trace_file", metavar ='TRACEFILE', 45 | help="trace file to pull GPX log from") 46 | parser.add_argument("-f", "--format", type=str, choices=['gpx'], 47 | default='gpx', help="select the output format of the GPS trace") 48 | arguments = parser.parse_args() 49 | return arguments 50 | 51 | 52 | def main(): 53 | configure_logging() 54 | arguments = parse_options() 55 | 56 | transcoder = GPXTranscoder() 57 | source = TraceDataSource(transcoder.receive, filename=arguments.trace_file, 58 | loop=False, realtime=False) 59 | source.start() 60 | source.join() 61 | 62 | print((transcoder.output().decode("utf-8"))) 63 | -------------------------------------------------------------------------------- /openxc/tools/obd2scanner.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the methods for the ``openxc-obd2scanner`` command line 3 | program. 4 | 5 | `main` is executed when ``openxc-obd2scanner`` is run, and all other callables in this 6 | module are internal only. 7 | """ 8 | 9 | import sys 10 | import argparse 11 | 12 | from .common import device_options, configure_logging, select_device 13 | import json 14 | 15 | import functools 16 | print = functools.partial(print, flush=True) 17 | 18 | def scan(controller, bus=None): 19 | 20 | # TODO could read the response from the "PIDs supported" requests to see 21 | # what the vehicle reports that it *should* support. 22 | print("Beginning sequential scan of all OBD-II PIDs") 23 | for pid in range(0, 0x88): 24 | response = controller.create_diagnostic_request(0x7df, mode=0x1, bus=bus, wait_for_first_response=True, pid=pid) 25 | if response is not None: 26 | no_response = True 27 | for item in response[1]: 28 | if 'success' in item: 29 | no_response = False 30 | print("PID 0x%x responded with: %s" % (pid, item)) 31 | 32 | if (no_response == True): 33 | print("PID 0x%x did not respond" % pid) 34 | 35 | def parse_options(): 36 | parser = argparse.ArgumentParser(description="Send requests for all " 37 | "OBD-II PIDs sequentially to see what actually responds", 38 | parents=[device_options()]) 39 | parser.add_argument("--bus") 40 | 41 | return parser.parse_args() 42 | 43 | 44 | def main(): 45 | configure_logging() 46 | arguments = parse_options() 47 | 48 | controller_class, controller_kwargs = select_device(arguments) 49 | controller = controller_class(**controller_kwargs) 50 | controller.start() 51 | 52 | scan(controller, arguments.bus) 53 | -------------------------------------------------------------------------------- /openxc/tools/scanner.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the methods for the ``openxc-scanner`` command line 3 | program. 4 | 5 | `main` is executed when ``openxc-scanner`` is run, and all other callables in this 6 | module are internal only. 7 | """ 8 | 9 | import sys 10 | import argparse 11 | from collections import defaultdict 12 | 13 | from .common import device_options, configure_logging, select_device 14 | 15 | import functools 16 | print = functools.partial(print, flush=True) 17 | 18 | TESTER_PRESENT_MODE = 0x3e 19 | TESTER_PRESENT_PAYLOAD = bytearray([0]) 20 | 21 | def find_active_modules(controller, bus, message_ids): 22 | active_modules = set() 23 | for arb_id in message_ids: 24 | response = controller.create_diagnostic_request(arb_id, TESTER_PRESENT_MODE, 25 | bus=bus, wait_for_first_response=True, 26 | payload=TESTER_PRESENT_PAYLOAD) 27 | if response is not None: 28 | print(("0x%x responded to tester present: %s" % (arb_id, response))) 29 | active_modules.add(arb_id) 30 | return active_modules 31 | 32 | def find_active_modes(controller, bus, active_modules): 33 | active_modes = defaultdict(list) 34 | for active_module in active_modules: 35 | controller.create_diagnostic_request(active_module, TESTER_PRESENT_MODE, 36 | bus=bus, frequency=1, wait_for_first_response=True, 37 | payload=TESTER_PRESENT_PAYLOAD) 38 | # TODO don't really care about response, but need to wait before sending 39 | # the next request or we will get a pipe error on USB 40 | 41 | print(("Scanning services on 0x%x" % active_module)) 42 | for mode in range(1, 0xff): 43 | # TODO should we be sending blank requests to each mode, or should 44 | # we send tester present with the service ID specified? 45 | response = controller.create_diagnostic_request(active_module, mode, bus=bus, 46 | wait_for_first_response=True) 47 | if response is not None: 48 | print(("0x%x responded on service 0x%x" % (active_module, mode))) 49 | # TODO make sure response isn't negative 50 | active_modes[active_module].append(mode) 51 | 52 | controller.create_diagnostic_request(active_module, TESTER_PRESENT_MODE, bus=bus, 53 | frequency=0) 54 | return active_modes 55 | 56 | def scan(controller, bus=None, message_id=None): 57 | message_ids = [] 58 | if message_id is not None: 59 | message_ids.append(message_id) 60 | else: 61 | # using 11-bit IDs 62 | message_ids = list(range(0, 0x7ff + 1)) 63 | 64 | print("Sending tester present message to find valid modules arb IDs") 65 | active_modules = find_active_modules(controller, bus, message_ids) 66 | 67 | # Scan for active services on each active module by sending blank requests 68 | print("Scanning for services on active modules") 69 | active_modes = find_active_modes(controller, bus, active_modules) 70 | 71 | # Scan for what each mode can do and what data it can return by fuzzing the 72 | # payloads 73 | print("Fuzzing the valid modes on acitve modules to see what happens") 74 | for arb_id, active_modes in active_modes.items(): 75 | controller.create_diagnostic_request(arb_id, TESTER_PRESENT_MODE, bus=bus, 76 | frequency=1, payload=TESTER_PRESENT_PAYLOAD) 77 | 78 | for mode in active_modes: 79 | # TODO how to support more payloads efficiently? 80 | for payload in range(0, 0xfffff): 81 | response = controller.create_diagnostic_request(arb_id, mode, 82 | bus=bus, payload=payload, wait_for_first_response=True) 83 | if response is not None: 84 | # TODO make sure response isn't negative 85 | # TODO print out something? 86 | print(("0x%x responded to mode 0x%x request with payload 0x%x with: %s" % (arb_id, mode, payload, response))) 87 | 88 | controller.create_diagnostic_request(arb_id, TESTER_PRESENT_MODE, bus=bus, 89 | frequency=0) 90 | 91 | def parse_options(): 92 | parser = argparse.ArgumentParser(description="Send diagnostic message requests to an attached VI", 93 | parents=[device_options()]) 94 | parser.add_argument("--bus", help="CAN bus controller address to send on") 95 | parser.add_argument("--message-id", help="CAN message ID for the request") 96 | 97 | return parser.parse_args() 98 | 99 | 100 | def main(): 101 | try: 102 | configure_logging() 103 | arguments = parse_options() 104 | 105 | controller_class, controller_kwargs = select_device(arguments) 106 | controller = controller_class(**controller_kwargs) 107 | controller.start() 108 | while(True): 109 | scan(controller, arguments.bus, arguments.message_id) 110 | except KeyboardInterrupt: 111 | sys.exit(0) 112 | -------------------------------------------------------------------------------- /openxc/tools/static/css/dashboard.css: -------------------------------------------------------------------------------- 1 | #page-contents { 2 | display: flex; 3 | justify-content: space-evenly; 4 | height: 100%; 5 | margin-top: 3%; 6 | } 7 | 8 | form { 9 | display: table; 10 | } 11 | 12 | .error { 13 | color: red; 14 | display: block; 15 | font-size: 0.85em; 16 | font-style: italic; 17 | } 18 | 19 | form li { 20 | list-style-type: none; 21 | display: table-row; 22 | } 23 | 24 | form label, input { 25 | display: table-cell; 26 | } 27 | 28 | #dashboardSettingsSubmitBtn { 29 | margin-left: 9%; 30 | } 31 | 32 | caption { 33 | font-size: 1.5em; 34 | margin-bottom: 4.5%; 35 | } 36 | 37 | table, th, td { 38 | text-align: left; 39 | border-spacing: 8px 1px; 40 | border: 1px solid black; 41 | } 42 | 43 | .metric { 44 | text-align: right; 45 | } 46 | 47 | th:hover { 48 | cursor: pointer; 49 | background-color: #AAA; 50 | } 51 | 52 | table { 53 | padding-top: 2%; 54 | padding-bottom: 2%; 55 | } 56 | -------------------------------------------------------------------------------- /openxc/tools/static/js/dashboard.js: -------------------------------------------------------------------------------- 1 | let dataPoints = {}; 2 | 3 | /* --- Dashboard parameters ------- */ 4 | let justChangedHighlightDuration; 5 | let recentlyChangedHighlightDuration; 6 | let diagnosticCount = 0; 7 | /* --- End dashboard parameters --- */ 8 | 9 | var valueChangedTimer; 10 | var valueRecentlyChangedTimer; 11 | 12 | $(document).ready(function() { 13 | updateDashboardParameters(); 14 | searchTable() 15 | 16 | var f_sl = 1; 17 | var f_nm = 1; 18 | $("#sn").click(function(){ 19 | f_sl *= -1; 20 | var n = $(this).prevAll().length; 21 | sortTable(f_sl,n); 22 | }); 23 | $("#sv").click(function(){ 24 | f_nm *= -1; 25 | var n = $(this).prevAll().length; 26 | sortTable(f_nm,n); 27 | }); 28 | $("#sr").click(function(){ 29 | f_nm *= -1; 30 | var n = $(this).prevAll().length; 31 | sortTable(f_nm,n); 32 | }); 33 | $("#sf").click(function(){ 34 | f_nm *= -1; 35 | var n = $(this).prevAll().length; 36 | sortTable(f_nm,n); 37 | }); 38 | 39 | 40 | var namespace = ''; 41 | var socket = io(namespace); 42 | socket.on('vehicle data', function(msg, cb) { 43 | // console.log(msg); 44 | 45 | if (!msg.hasOwnProperty('command_response')) { 46 | if (msg.hasOwnProperty('success')) { 47 | // Using the 'success' property to identify a diagnostic response 48 | diagnosticCount++; 49 | let diagnosticName = 'diagnostic_' + diagnosticCount; 50 | addDiagnosticResponse(diagnosticName, msg); 51 | } else { 52 | if (!msg.hasOwnProperty('name')) { 53 | msg.name = 'Raw-' + msg.bus + '-0x' + msg.id.toString(16); 54 | msg.value = msg.data; 55 | } 56 | 57 | if (msg.hasOwnProperty('event')) { 58 | msg.value = msg.value + ': ' + msg.event 59 | } 60 | 61 | if (!(msg.name in dataPoints)) { 62 | dataPoints[msg.name] = { 63 | current_data: undefined, 64 | events: {}, 65 | messages_received: 0, 66 | measurement_type: undefined, 67 | min: undefined, 68 | max: undefined, 69 | last_update_time: undefined, 70 | average_time_since_update: undefined 71 | }; 72 | } 73 | 74 | updateDataPoint(dataPoints[msg.name], msg); 75 | updateDisplay(dataPoints[msg.name]); 76 | 77 | if (cb) { 78 | cb(); 79 | } 80 | } 81 | } 82 | }); 83 | }); 84 | 85 | function updateDashboardParameters() { 86 | valueChangedTimer = Number($('#justChangedHighlightDuration').val()); 87 | valueRecentlyChangedTimer = Number($('#recentlyChangedHighlightDuration').val()); 88 | } 89 | 90 | function saveSettings(e) { 91 | e.preventDefault(); 92 | updateDashboardParameters(); 93 | } 94 | 95 | function addToDisplay(msgName) { 96 | $('', { 97 | id: msgName 98 | }).appendTo('#log'); 99 | 100 | $('', { 101 | id: msgName + '_label', 102 | text: msgName 103 | }).appendTo('#' + msgName); 104 | 105 | $('', { 106 | id: msgName + '_value' 107 | }).appendTo('#' + msgName); 108 | 109 | $('', { 110 | id: msgName + '_num', 111 | class: 'metric' 112 | }).appendTo('#' + msgName); 113 | 114 | $('', { 115 | id: msgName + '_freq', 116 | class: 'metric' 117 | }).appendTo('#' + msgName); 118 | } 119 | 120 | function updateDisplay(dataPoint) { 121 | var msg = dataPoint.current_data 122 | 123 | if ($('#' + msg.name).length <= 0) { 124 | addToDisplay(msg.name); 125 | } 126 | 127 | $('#' + msg.name + '_value').text(msg.value); 128 | highlightCell('#' + msg.name + '_value'); 129 | 130 | $('#' + msg.name + '_num').text(dataPoint.messages_received); 131 | $('#' + msg.name + '_freq').text(Math.ceil(1 / dataPoint.average_time_since_update)); 132 | } 133 | 134 | function highlightCell(cellId) { 135 | $(cellId).stop(true); 136 | $(cellId).css({'background': '#1338F0', 'color': 'white'}); 137 | $(cellId).animate({backgroundColor: '#949494'}, valueChangedTimer, function() { 138 | $(this).animate({backgroundColor: '#FFFFFF', color: 'black'}, valueRecentlyChangedTimer); 139 | }); 140 | } 141 | 142 | function validateSettingsForm() { 143 | let valid = true; 144 | let errors = []; 145 | 146 | $('.error').each(function() { 147 | $(this).text(''); 148 | }); 149 | 150 | errors = validateTimerInput($('#justChangedHighlightDuration'), errors); 151 | errors = validateTimerInput($('#recentlyChangedHighlightDuration'), errors); 152 | 153 | if (errors.length > 0) { 154 | valid = false; 155 | errors.forEach(function(e) { 156 | $('#' + e.id + '_error').text(e.msg); 157 | }); 158 | } 159 | 160 | return valid; 161 | } 162 | 163 | function validateTimerInput(input, errors) { 164 | let inputVal = input.val(); 165 | 166 | if (isNaN(inputVal) || inputVal < 0) { 167 | errors.push({id: input[0].id, msg: 'Input must be a positive number'}); 168 | } 169 | 170 | return errors; 171 | } 172 | 173 | function updateDataPoint(dataPoint, measurement) { 174 | dataPoint.messages_received++; 175 | dataPoint.current_data = measurement; 176 | let update_time = (new Date()).getTime() / 1000; 177 | 178 | if (dataPoint.last_update_time !== undefined) { 179 | dataPoint.average_time_since_update = 180 | calculateAverageTimeSinceUpdate(update_time, dataPoint); 181 | } 182 | 183 | dataPoint.last_update_time = update_time; 184 | 185 | if ('event' in measurement) { 186 | dataPoint.events[measurement.value] = measurement.event; 187 | } 188 | } 189 | 190 | function calculateAverageTimeSinceUpdate(updateTime, dataPoint) { 191 | let time_since_update = updateTime - dataPoint.last_update_time; 192 | 193 | return (dataPoint.average_time_since_update === undefined) 194 | ? time_since_update 195 | : (0.1 * time_since_update) + (0.9 * dataPoint.average_time_since_update); 196 | } 197 | 198 | function sortTable(f,n){ 199 | var rows = $('#log tbody tr').get(); 200 | 201 | rows.sort(function(a, b) { 202 | 203 | var A = getVal(a); 204 | var B = getVal(b); 205 | 206 | if(A < B) { 207 | return -1*f; 208 | } 209 | if(A > B) { 210 | return 1*f; 211 | } 212 | return 0; 213 | }); 214 | 215 | function getVal(elm){ 216 | var v = $(elm).children('td').eq(n).text().toUpperCase(); 217 | if($.isNumeric(v)){ 218 | v = parseInt(v,10); 219 | } 220 | return v; 221 | } 222 | 223 | $.each(rows, function(index, row) { 224 | $('#log').children('tbody').append(row); 225 | }); 226 | console.log("jamez test"); 227 | } 228 | 229 | function searchTable() { 230 | $("#myInput").on("keyup", function() { 231 | var value = $(this).val().toLowerCase(); 232 | $("#log tr").filter(function() { 233 | $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1) 234 | }); 235 | }); 236 | } 237 | 238 | function addDiagnosticResponse(name, message) { 239 | $('', { 240 | id: name 241 | }).appendTo('#diagnostic'); 242 | 243 | $('', { 244 | id: name + '_bus', 245 | text: message.bus 246 | }).appendTo('#' + name); 247 | 248 | $('', { 249 | id: name + '_id', 250 | text: message.id 251 | }).appendTo('#' + name); 252 | 253 | $('', { 254 | id: name + '_mode', 255 | text: message.mode 256 | }).appendTo('#' + name); 257 | 258 | $('', { 259 | id: name + '_pid', 260 | text: message.pid 261 | }).appendTo('#' + name); 262 | 263 | $('', { 264 | id: name + '_success', 265 | text: message.success 266 | }).appendTo('#' + name); 267 | 268 | $('', { 269 | id: name + '_payload', 270 | text: message.payload 271 | }).appendTo('#' + name); 272 | 273 | $('', { 274 | id: name + '_value', 275 | text: message.value 276 | }).appendTo('#' + name); 277 | 278 | if (message.success == false) { 279 | $('', { 280 | id: name + '_neg_resp_code', 281 | text: message.negative_response_code 282 | }).appendTo('#' + name); 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /openxc/tools/static/js/jquery.color-2.1.2.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery Color v@2.1.2 http://github.com/jquery/jquery-color | jquery.org/license */ 2 | (function(a,b){function m(a,b,c){var d=h[b.type]||{};return a==null?c||!b.def?null:b.def:(a=d.floor?~~a:parseFloat(a),isNaN(a)?b.def:d.mod?(a+d.mod)%d.mod:0>a?0:d.max")[0],k,l=a.each;j.style.cssText="background-color:rgba(1,1,1,.5)",i.rgba=j.style.backgroundColor.indexOf("rgba")>-1,l(g,function(a,b){b.cache="_"+a,b.props.alpha={idx:3,type:"percent",def:1}}),f.fn=a.extend(f.prototype,{parse:function(c,d,e,h){if(c===b)return this._rgba=[null,null,null,null],this;if(c.jquery||c.nodeType)c=a(c).css(d),d=b;var i=this,j=a.type(c),o=this._rgba=[];d!==b&&(c=[c,d,e,h],j="array");if(j==="string")return this.parse(n(c)||k._default);if(j==="array")return l(g.rgba.props,function(a,b){o[b.idx]=m(c[b.idx],b)}),this;if(j==="object")return c instanceof f?l(g,function(a,b){c[b.cache]&&(i[b.cache]=c[b.cache].slice())}):l(g,function(b,d){var e=d.cache;l(d.props,function(a,b){if(!i[e]&&d.to){if(a==="alpha"||c[a]==null)return;i[e]=d.to(i._rgba)}i[e][b.idx]=m(c[a],b,!0)}),i[e]&&a.inArray(null,i[e].slice(0,3))<0&&(i[e][3]=1,d.from&&(i._rgba=d.from(i[e])))}),this},is:function(a){var b=f(a),c=!0,d=this;return l(g,function(a,e){var f,g=b[e.cache];return g&&(f=d[e.cache]||e.to&&e.to(d._rgba)||[],l(e.props,function(a,b){if(g[b.idx]!=null)return c=g[b.idx]===f[b.idx],c})),c}),c},_space:function(){var a=[],b=this;return l(g,function(c,d){b[d.cache]&&a.push(c)}),a.pop()},transition:function(a,b){var c=f(a),d=c._space(),e=g[d],i=this.alpha()===0?f("transparent"):this,j=i[e.cache]||e.to(i._rgba),k=j.slice();return c=c[e.cache],l(e.props,function(a,d){var e=d.idx,f=j[e],g=c[e],i=h[d.type]||{};if(g===null)return;f===null?k[e]=g:(i.mod&&(g-f>i.mod/2?f+=i.mod:f-g>i.mod/2&&(f-=i.mod)),k[e]=m((g-f)*b+f,d))}),this[d](k)},blend:function(b){if(this._rgba[3]===1)return this;var c=this._rgba.slice(),d=c.pop(),e=f(b)._rgba;return f(a.map(c,function(a,b){return(1-d)*e[b]+d*a}))},toRgbaString:function(){var b="rgba(",c=a.map(this._rgba,function(a,b){return a==null?b>2?1:0:a});return c[3]===1&&(c.pop(),b="rgb("),b+c.join()+")"},toHslaString:function(){var b="hsla(",c=a.map(this.hsla(),function(a,b){return a==null&&(a=b>2?1:0),b&&b<3&&(a=Math.round(a*100)+"%"),a});return c[3]===1&&(c.pop(),b="hsl("),b+c.join()+")"},toHexString:function(b){var c=this._rgba.slice(),d=c.pop();return b&&c.push(~~(d*255)),"#"+a.map(c,function(a){return a=(a||0).toString(16),a.length===1?"0"+a:a}).join("")},toString:function(){return this._rgba[3]===0?"transparent":this.toRgbaString()}}),f.fn.parse.prototype=f.fn,g.hsla.to=function(a){if(a[0]==null||a[1]==null||a[2]==null)return[null,null,null,a[3]];var b=a[0]/255,c=a[1]/255,d=a[2]/255,e=a[3],f=Math.max(b,c,d),g=Math.min(b,c,d),h=f-g,i=f+g,j=i*.5,k,l;return g===f?k=0:b===f?k=60*(c-d)/h+360:c===f?k=60*(d-b)/h+120:k=60*(b-c)/h+240,h===0?l=0:j<=.5?l=h/i:l=h/(2-i),[Math.round(k)%360,l,j,e==null?1:e]},g.hsla.from=function(a){if(a[0]==null||a[1]==null||a[2]==null)return[null,null,null,a[3]];var b=a[0]/360,c=a[1],d=a[2],e=a[3],f=d<=.5?d*(1+c):d+c-d*c,g=2*d-f;return[Math.round(o(g,f,b+1/3)*255),Math.round(o(g,f,b)*255),Math.round(o(g,f,b-1/3)*255),e]},l(g,function(c,e){var g=e.props,h=e.cache,i=e.to,j=e.from;f.fn[c]=function(c){i&&!this[h]&&(this[h]=i(this._rgba));if(c===b)return this[h].slice();var d,e=a.type(c),k=e==="array"||e==="object"?c:arguments,n=this[h].slice();return l(g,function(a,b){var c=k[e==="object"?a:b.idx];c==null&&(c=n[b.idx]),n[b.idx]=m(c,b)}),j?(d=f(j(n)),d[h]=n,d):f(n)},l(g,function(b,e){if(f.fn[b])return;f.fn[b]=function(f){var g=a.type(f),h=b==="alpha"?this._hsla?"hsla":"rgba":c,i=this[h](),j=i[e.idx],k;return g==="undefined"?j:(g==="function"&&(f=f.call(this,j),g=a.type(f)),f==null&&e.empty?this:(g==="string"&&(k=d.exec(f),k&&(f=j+parseFloat(k[2])*(k[1]==="+"?1:-1))),i[e.idx]=f,this[h](i)))}})}),f.hook=function(b){var c=b.split(" ");l(c,function(b,c){a.cssHooks[c]={set:function(b,d){var e,g,h="";if(d!=="transparent"&&(a.type(d)!=="string"||(e=n(d)))){d=f(e||d);if(!i.rgba&&d._rgba[3]!==1){g=c==="backgroundColor"?b.parentNode:b;while((h===""||h==="transparent")&&g&&g.style)try{h=a.css(g,"backgroundColor"),g=g.parentNode}catch(j){}d=d.blend(h&&h!=="transparent"?h:"_default")}d=d.toRgbaString()}try{b.style[c]=d}catch(j){}}},a.fx.step[c]=function(b){b.colorInit||(b.start=f(b.elem,c),b.end=f(b.end),b.colorInit=!0),a.cssHooks[c].set(b.elem,b.start.transition(b.end,b.pos))}})},f.hook(c),a.cssHooks.borderColor={expand:function(a){var b={};return l(["Top","Right","Bottom","Left"],function(c,d){b["border"+d+"Color"]=a}),b}},k=a.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}})(jQuery); -------------------------------------------------------------------------------- /openxc/tools/templates/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenXC Dashboard 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |

OpenXC Dashboard

15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
Signal NameValue# ReceivedFreq. (Hz)
28 |
29 |
30 |

Settings

31 |
32 |

Highlight Duration (ms)

33 |
    34 |
  • 35 | 36 | 37 | 38 |
  • 39 |
  • 40 | 41 | 42 | 43 |
  • 44 |
45 | 46 |
47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
Diagnostic Responses
BusIDModePIDSuccessPayloadValueNeg. Resp. Code
62 |
63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /openxc/tools/tracesplit.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the methods for the ``openxc-trace-split`` command line 3 | program. 4 | 5 | `main` is executed when ``openxc-trace-split`` is run, and all other callables 6 | in this module are internal only. 7 | """ 8 | 9 | 10 | import argparse 11 | import datetime 12 | from operator import itemgetter 13 | from collections import defaultdict 14 | 15 | from openxc.formats.json import JsonFormatter 16 | from openxc.sources.trace import TraceDataSource 17 | from .common import configure_logging 18 | 19 | 20 | class BaseSplitter(object): 21 | def __init__(self): 22 | self.records = [] 23 | self.buckets = defaultdict(list) 24 | 25 | def _key_for_record(self, record): 26 | raise NotImplementedError 27 | 28 | def split(self, files): 29 | for filename in files: 30 | source = TraceDataSource(self.receive, filename=filename, 31 | loop=False, realtime=False) 32 | source.start() 33 | 34 | self.records.sort(key=itemgetter('timestamp')) 35 | for record in self.records: 36 | self.buckets[self._key_for_record(record)].append(record) 37 | 38 | return self.buckets 39 | 40 | def receive(self, message, **kwargs): 41 | self.records.append(message) 42 | 43 | 44 | class TimeSplitter(BaseSplitter): 45 | def __init__(self, unit): 46 | super(TimeSplitter, self).__init__() 47 | self.unit = unit 48 | 49 | def _key_for_record(self, record): 50 | date = datetime.datetime.fromtimestamp(record['timestamp']) 51 | if self.unit == "day": 52 | date_format = '%Y-%m-%d' 53 | elif self.unit == "hour": 54 | date_format = '%Y-%m-%d-%H' 55 | return date.strftime(date_format) 56 | 57 | 58 | class TripSplitter(BaseSplitter): 59 | # TODO allow overriding this at the command line 60 | MAXIMUM_RECORD_GAP_SECONDS = 600 61 | 62 | def _key_for_record(self, record): 63 | timestamp = record['timestamp'] 64 | last_timestamp = getattr(self, 'last_timestamp', None) 65 | if last_timestamp is not None: 66 | if (timestamp - last_timestamp) > self.MAXIMUM_RECORD_GAP_SECONDS: 67 | self.current_trip_key = timestamp 68 | else: 69 | self.current_trip_key = timestamp 70 | self.last_timestamp = timestamp 71 | return datetime.datetime.fromtimestamp(self.last_timestamp).strftime( 72 | "%Y-%m-%d-%H.%M") 73 | 74 | 75 | def parse_options(): 76 | parser = argparse.ArgumentParser(description="Split a collection of " 77 | "OpenXC trace files by day, hour or trips") 78 | parser.add_argument("files", action="store", nargs='+', default=False) 79 | parser.add_argument("-s", "--split", action="store", 80 | choices=['day', 'hour', 'trip'], default="trip", 81 | help="select the time unit to split the combined trace files") 82 | 83 | arguments = parser.parse_args() 84 | return arguments 85 | 86 | 87 | def main(): 88 | configure_logging() 89 | arguments = parse_options() 90 | 91 | if arguments.split == 'trip': 92 | splitter = TripSplitter() 93 | else: 94 | splitter = TimeSplitter(arguments.split) 95 | 96 | for key, split in list(splitter.split(arguments.files).items()): 97 | with open("%s.json" % key, 'w') as output_file: 98 | for record in split: 99 | output_file.write(JsonFormatter.serialize(record) + "\n") 100 | -------------------------------------------------------------------------------- /openxc/units.py: -------------------------------------------------------------------------------- 1 | """Define the scalar units used by vehicle measurements.""" 2 | 3 | 4 | import sys 5 | from units import unit, scaled_unit, named_unit 6 | import units.predefined 7 | 8 | units.predefined.define_units() 9 | 10 | 11 | Percentage = unit('%') 12 | Meter = unit('m') 13 | Kilometer = scaled_unit('km', 'm', 1000) 14 | Hour = unit('h') 15 | KilometersPerHour = unit('km') / unit('h') 16 | RotationsPerMinute = unit('rotations') / unit('m') 17 | Litre = unit('L') 18 | Degree = unit('deg') 19 | NewtonMeter = named_unit("Nm", ["N", "m"], []) 20 | MetersPerSecondSquared = unit('m') / (pow(unit('s'), 2)) 21 | Undefined = unit('undef') 22 | -------------------------------------------------------------------------------- /openxc/utils.py: -------------------------------------------------------------------------------- 1 | """Data containers and other utilities.""" 2 | import time 3 | import json 4 | import collections 5 | import logging 6 | import os 7 | import sys 8 | 9 | LOG = logging.getLogger(__name__) 10 | 11 | class Range(object): 12 | """Encapsulates a ranged defined by a min and max numerical value.""" 13 | def __init__(self, minimum, maximum): 14 | self.min = minimum 15 | self.max = maximum 16 | 17 | def within_range(self, value): 18 | """Returns True if the value is between this Range, inclusive.""" 19 | return value >= self.min and value <= self.max 20 | 21 | @property 22 | def spread(self): 23 | """Returns the spread between this Range's min and max.""" 24 | return self.max - self.min 25 | 26 | 27 | class AgingData(object): 28 | """Mixin to associate a class with a time of birth.""" 29 | 30 | def __init__(self): 31 | self.created_at = time.time() 32 | 33 | @property 34 | def age(self): 35 | """Return the age of the data in seconds.""" 36 | return time.time() - self.created_at 37 | 38 | 39 | def quacks_like_dict(object): 40 | """Check if object is dict-like""" 41 | return isinstance(object, collections.Mapping) 42 | 43 | 44 | def quacks_like_list(object): 45 | """Check if object is list-like""" 46 | return hasattr(object, '__iter__') and hasattr(object, 'append') 47 | 48 | 49 | def merge(a, b): 50 | """Merge two deep dicts non-destructively 51 | 52 | Uses a stack to avoid maximum recursion depth exceptions 53 | 54 | >>> a = {'a': 1, 'b': {1: 1, 2: 2}, 'd': 6} 55 | >>> b = {'c': 3, 'b': {2: 7}, 'd': {'z': [1, 2, 3]}} 56 | >>> c = merge(a, b) 57 | >>> from pprint import pprint; pprint(c) 58 | {'a': 1, 'b': {1: 1, 2: 7}, 'c': 3, 'd': {'z': [1, 2, 3]}} 59 | """ 60 | assert quacks_like_dict(a), quacks_like_dict(b) 61 | dst = a.copy() 62 | 63 | stack = [(dst, b)] 64 | while stack: 65 | current_dst, current_src = stack.pop() 66 | for key in current_src: 67 | if key not in current_dst: 68 | current_dst[key] = current_src[key] 69 | else: 70 | if (quacks_like_dict(current_src[key]) and 71 | quacks_like_dict(current_dst[key])): 72 | stack.append((current_dst[key], current_src[key])) 73 | elif (quacks_like_list(current_src[key]) and 74 | quacks_like_list(current_dst[key])): 75 | current_dst[key].extend(current_src[key]) 76 | else: 77 | current_dst[key] = current_src[key] 78 | return dst 79 | 80 | 81 | def find_file(filename, search_paths): 82 | for search_path in search_paths: 83 | if filename[0] != '/': 84 | full_path = "%s/%s" % (search_path, filename) 85 | else: 86 | full_path = filename 87 | if os.path.exists(full_path): 88 | return full_path 89 | fatal_error("Unable to find '%s' in search paths (%s)" % ( 90 | filename, search_paths)) 91 | 92 | 93 | def dict_raise_on_duplicates(ordered_pairs): 94 | """Reject duplicate keys.""" 95 | d = {} 96 | for k, v in ordered_pairs: 97 | if k in d: 98 | raise ValueError("duplicate key: %r" % (k,)) 99 | else: 100 | d[k] = v 101 | return d 102 | 103 | def load_json_from_search_path(filename, search_paths): 104 | with open(find_file(filename, search_paths)) as json_file: 105 | try: 106 | data = json.load(json_file, object_pairs_hook=dict_raise_on_duplicates) 107 | except ValueError as e: 108 | fatal_error("%s does not contain valid JSON: \n%s\n" % 109 | (filename, e)) 110 | else: 111 | return data 112 | 113 | def fatal_error(message): 114 | LOG.error(message) 115 | sys.exit(1) 116 | -------------------------------------------------------------------------------- /openxc/vehicle.py: -------------------------------------------------------------------------------- 1 | """This module is contains the Vehicle class, which is the main entry point for 2 | using the Python library to access vehicle data programatically. Most users will 3 | want to interact with an instance of Vehicle, and won't need to deal with other 4 | parts of the library directly (besides measurement types). 5 | """ 6 | from .measurements import Measurement 7 | from .sinks import MeasurementNotifierSink 8 | 9 | class Vehicle(object): 10 | """The Vehicle class is the main entry point for the OpenXC Python library. 11 | A Vehicle represents a connection to at least one vehicle data source and 12 | zero or 1 vehicle controllers, which can accept commands to send back to the 13 | vehicle. A Vehicle instance can have more than one data source (e.g. if the 14 | computer using this library has a secondary GPS data source). 15 | 16 | Most applications will either request synchronous vehicle data measurements 17 | using the ``get`` method or or with a callback function passed to 18 | ``listen``. 19 | 20 | More advanced applications that want access to all raw vehicle data may want 21 | to register a ``DataSink`` with a Vehicle. 22 | """ 23 | 24 | def __init__(self, interface=None): 25 | """Construct a new Vehicle instance, optionally providing an vehicle 26 | interface from ``openxc.interface`` to user for I/O. 27 | """ 28 | self.sources = set() 29 | self.sinks = set() 30 | self.measurements = {} 31 | 32 | if interface is not None: 33 | self.add_source(interface) 34 | self.controller = interface 35 | 36 | self.notifier = MeasurementNotifierSink() 37 | self.sinks.add(self.notifier) 38 | 39 | def get(self, measurement_class): 40 | """Return the latest measurement for the given class or None if nothing 41 | has been received from the vehicle. 42 | """ 43 | name = Measurement.name_from_class(measurement_class) 44 | return self._construct_measurement(name) 45 | 46 | def listen(self, measurement_class, callback): 47 | """Register the callback function to be called whenever a new 48 | measurement of the given class is received from the vehicle data 49 | sources. 50 | 51 | If the callback is already registered for measurements of the given 52 | type, this method will have no effect. 53 | """ 54 | self.notifier.register(measurement_class, callback) 55 | 56 | def unlisten(self, measurement_class, callback): 57 | """Stop notifying the given callback of new values of the measurement 58 | type. 59 | 60 | If the callback was not previously registered as a listener, this method 61 | will have no effect. 62 | """ 63 | self.notifier.unregister(measurement_class, callback) 64 | 65 | def add_source(self, source): 66 | """Add a vehicle data source to the instance. 67 | 68 | The Vehicle instance will be set as the callback of the source, and the 69 | source will be started if it is startable. (i.e. it has a ``start()`` 70 | method). 71 | """ 72 | if source is not None: 73 | self.sources.add(source) 74 | source.callback = self._receive 75 | if hasattr(source, 'start'): 76 | source.start() 77 | 78 | def add_sink(self, sink): 79 | """Add a vehicle data sink to the instance. ``sink`` should be a 80 | sub-class of ``DataSink`` or at least have a ``receive(message, 81 | **kwargs)`` method. 82 | 83 | The sink will be started if it is startable. (i.e. it has a ``start()`` 84 | method). 85 | """ 86 | if sink is not None: 87 | self.sinks.add(sink) 88 | if hasattr(sink, 'start'): 89 | sink.start() 90 | 91 | def _receive(self, message, **kwargs): 92 | name = message.get('name', 'can_message') 93 | self.measurements[name] = message 94 | 95 | for sink in self.sinks: 96 | sink.receive(message, **kwargs) 97 | 98 | def _construct_measurement(self, measurement_id): 99 | raw_measurement = self.measurements.get(measurement_id, None) 100 | if raw_measurement is not None: 101 | return Measurement.from_dict(raw_measurement) 102 | -------------------------------------------------------------------------------- /openxc/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Current OpenXC version constant. 3 | 4 | This functionality is contained in its own module to prevent circular import 5 | problems with ``__init__.py`` (which is loaded by setup.py during installation, 6 | which in turn needs access to this version information.) 7 | """ 8 | 9 | VERSION = (2, 2, 0) 10 | 11 | __version__ = '.'.join(map(str, VERSION)) 12 | 13 | def get_version(): 14 | return __version__ 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools import setup, find_packages 3 | 4 | from openxc.version import get_version 5 | 6 | # This is a workaround for an odd exception that occurs when running the tests. 7 | try: 8 | import multiprocessing 9 | except ImportError: 10 | pass 11 | 12 | long_description = open('README.rst').read() 13 | 14 | install_reqs = ['pyusb==1.0.0a3', 'units >= 0.5', 'argparse', 'requests==2.20.0', 15 | 'protobuf==3.17.1'] 16 | 17 | setup(name='openxc', 18 | version=get_version(), 19 | description='A Python library to connect to an OpenXC vehicle interface', 20 | long_description=long_description, 21 | long_description_content_type="text/x-rst", 22 | author='Christopher Peplin', 23 | author_email='cpeplin@ford.com', 24 | license='BSD', 25 | classifiers = [ 26 | 'Development Status :: 4 - Beta', 27 | 'License :: OSI Approved :: BSD License', 28 | 'Operating System :: OS Independent', 29 | 'Programming Language :: Python', 30 | 'Topic :: Software Development', 31 | ], 32 | url='http://github.com/openxc/openxc-python', 33 | packages=find_packages(exclude=["tests", "tests.*"]), 34 | package_data={'openxc': ['generator/signals.cpp*', 'tools/templates/dashboard.html*', 'tools/static/css/dashboard.css*', 'tools/static/js/dashboard.js*', 'tools/static/js/socket.io.slim.js*', 'tools/static/js/jquery-3.4.1.min.js*', 'tools/static/js/jquery.color-2.1.2.min.js']}, 35 | test_suite='nose.collector', 36 | tests_require=['nose'], 37 | install_requires=install_reqs, 38 | extras_require = { 39 | 'serial': ["pyserial==3.1.1"], 40 | 'bluetooth': ["pybluez"], 41 | 'lxml': ["lxml"], 42 | }, 43 | entry_points={ 44 | 'console_scripts': [ 45 | 'openxc-dashboard = openxc.tools.dashboard:main', 46 | 'openxc-dump = openxc.tools.dump:main', 47 | 'openxc-control = openxc.tools.control:main', 48 | 'openxc-gps = openxc.tools.gps:main', 49 | 'openxc-trace-split = openxc.tools.tracesplit:main', 50 | 'openxc-generate-firmware-code = openxc.tools.generate_code:main', 51 | 'openxc-diag = openxc.tools.diagnostics:main', 52 | 'openxc-scanner = openxc.tools.scanner:main', 53 | 'openxc-obd2scanner = openxc.tools.obd2scanner:main', 54 | ] 55 | }, 56 | ) 57 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openxc/openxc-python/5341180fea6c364027dedc9bc4c8027b2831325f/tests/__init__.py -------------------------------------------------------------------------------- /tests/diagnostic.json.example: -------------------------------------------------------------------------------- 1 | { "name": "example_diagnostic", 2 | "buses": { 3 | "hs": { 4 | "controller": 1, 5 | "raw_writable": true, 6 | "speed": 500000 7 | } 8 | }, 9 | "diagnostic_messages": [ 10 | { 11 | "bus": "hs", 12 | "id": 2015, 13 | "mode": 1, 14 | "pid": 12, 15 | "frequency": 1, 16 | "decoder": "handleObd2Pid" 17 | }, 18 | { 19 | "bus": "hs", 20 | "id": 2016, 21 | "mode": 2, 22 | "pid": 12, 23 | "frequency": 1 24 | }, 25 | { 26 | "bus": "hs", 27 | "id": 2016, 28 | "mode": 22, 29 | "pid": 6, 30 | "frequency": 1, 31 | "decoder": "handleMyDiagRequest" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /tests/evented.json: -------------------------------------------------------------------------------- 1 | {"name": "button_event", "value": "up", "event": "pressed"} 2 | {"name": "button_event", "value": "up", "event": "released"} 3 | -------------------------------------------------------------------------------- /tests/handlers.cpp.example: -------------------------------------------------------------------------------- 1 | #include "can/canread.h" 2 | 3 | using openxc::can::read::publishNumericalMessage; 4 | 5 | void handleSteeringWheelMessage(CanMessage* message, CanSignal* signals, 6 | int signalCount, Pipeline* pipeline) { 7 | publishNumericalMessage("latitude", 42.0, pipeline); 8 | } 9 | 10 | openxc_DynamicField handleInverted(CanSignal* signal, CanSignal* signals, 11 | int signalCount, float value, bool* send) { 12 | return openxc::payload::wrapNumber(value * -1); 13 | } 14 | 15 | void initializeMyStuff() { } 16 | 17 | void initializeOtherStuff() { } 18 | 19 | void myLooper() { 20 | // this function will be called once each time through the main loop, after 21 | // all CAN message processing has been completed 22 | } 23 | -------------------------------------------------------------------------------- /tests/mapped_signal_set.json.example: -------------------------------------------------------------------------------- 1 | { "name": "example_platform", 2 | "initializers": [ 3 | "initializeMyStuff", 4 | "initializeOtherStuff" 5 | ], 6 | "loopers": [ 7 | "myLooper" 8 | ], 9 | "buses": { 10 | "hs": { 11 | "controller": 1, 12 | "speed": 500000 13 | }, 14 | "info": { 15 | "controller": 2, 16 | "raw_can_mode": "unfiltered", 17 | "speed": 500000 18 | } 19 | }, 20 | "commands": [ 21 | { "name": "turn_signal_status", 22 | "handler": "handleTurnSignalCommand" 23 | } 24 | ], 25 | "mappings": [ 26 | {"mapping": "mapped_signals.json.example", 27 | "bus": "hs"} 28 | ], 29 | "messages": { 30 | "0x400": { 31 | "name": "MessageOnInfo", 32 | "bus": "info" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/mapped_signals.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "messages": { 3 | "0x128": { 4 | "name": "ECM_z_5D2", 5 | "handlers": ["handleInvertedSteeringWheel","ignoreHandler","handleSteeringWheelMessage"], 6 | "signals": { 7 | "StrAnglAct": { 8 | "generic_name": "SteeringWheelAngle", 9 | "bit_position": 52, 10 | "bit_size": 12, 11 | "factor": 0.15392, 12 | "offset": 0 13 | }, 14 | "StrAnglSign": { 15 | "generic_name": "steering_angle_sign", 16 | "bit_position": 52, 17 | "bit_size": 12 18 | }, 19 | "EngSpd": { 20 | "generic_name": "engine_speed", 21 | "max_frequency": 5, 22 | "bit_position": 12, 23 | "bit_size": 8 24 | }, 25 | "GrshftPos": { 26 | "generic_name": "GearshiftPosition", 27 | "bit_position": 41, 28 | "bit_size": 3, 29 | "states": { 30 | "FIRST": [1], 31 | "SECOND": [2], 32 | "THIRD": [3], 33 | "FOURTH": [4], 34 | "REVERSE": [5], 35 | "NEUTRAL": [6]} 36 | }, 37 | "StrAnglErr": { 38 | "generic_name": "steering_wheel_angle_error", 39 | "bit_position": 44, 40 | "bit_size": 12 41 | } 42 | } 43 | }, 44 | "0x49": { 45 | "name": "BrkSt", 46 | "handlers": ["booleanHandler"], 47 | "signals": { 48 | "BrakePedalPosition": { 49 | "generic_name": "brake_pedal_status", 50 | "send_same": false, 51 | "bit_position": 0, 52 | "bit_size": 0 53 | } 54 | } 55 | }, 56 | "0x121": { 57 | "name": "Cluster", 58 | "signals": { 59 | "TurnSignalLeft": { 60 | "generic_name": "turn_signal_left", 61 | "bit_position": 0, 62 | "bit_size": 0, 63 | "writable": true, 64 | "encoder": "booleanWriter" 65 | }, 66 | "TurnSignalRight": { 67 | "generic_name": "turn_signal_right", 68 | "bit_position": 1, 69 | "bit_size": 0, 70 | "writable": true, 71 | "encoder": "booleanWriter" 72 | } 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/signals.json.example: -------------------------------------------------------------------------------- 1 | { "name": "example_messages", 2 | "raw_can_mode": "filtered", 3 | "max_message_frequency": 5, 4 | "extra_sources": [ 5 | "handlers.cpp.example" 6 | ], 7 | "initializers": [ 8 | "initializeMyStuff", 9 | "initializeOtherStuff" 10 | ], 11 | "loopers": [ 12 | "myLooper" 13 | ], 14 | "buses": { 15 | "hs": { 16 | "controller": 1, 17 | "speed": 500000 18 | }, 19 | "ms": { 20 | "controller": 2, 21 | "raw_can_mode": "off", 22 | "max_message_frequency": 0, 23 | "speed": 125000 24 | } 25 | }, 26 | "commands": [ 27 | { "name": "turn_signal_status", 28 | "handler": "handleTurnSignalCommand" 29 | } 30 | ], 31 | "diagnostic_messages": [ 32 | {"bus": "hs", 33 | "name": "obd2_engine_speed", 34 | "id": 2015, 35 | "mode": 1, 36 | "pid": 12, 37 | "frequency": 1} 38 | ], 39 | "messages": { 40 | "0x128": { 41 | "name": "ECM_z_5D2", 42 | "bus": "hs", 43 | "signals": { 44 | "StrAnglAct": { 45 | "generic_name": "SteeringWheelAngle", 46 | "bit_position": 52, 47 | "bit_size": 12, 48 | "factor": 0.15392, 49 | "offset": 0, 50 | "decoder": "handleInverted"}, 51 | "StrAnglSign": { 52 | "generic_name": "steering_angle_sign", 53 | "decoder": "ignoreDecoder", 54 | "bit_position": 52, 55 | "bit_size": 12}, 56 | "EngSpd": { 57 | "generic_name": "engine_speed", 58 | "ignore": true, 59 | "bit_position": 12, 60 | "bit_size": 8}, 61 | "SomethingWeDontWant": { 62 | "generic_name": "dont_want", 63 | "bit_position": 12, 64 | "bit_size": 8}, 65 | "GrshftPos": { 66 | "generic_name": "GearshiftPosition", 67 | "bit_position": 41, 68 | "bit_size": 3, 69 | "states": { 70 | "FIRST": [1], 71 | "SECOND": [2], 72 | "THIRD": [3], 73 | "FOURTH": [4], 74 | "REVERSE": [5], 75 | "NEUTRAL": [6]} 76 | }, 77 | "IgnoredPos": { 78 | "generic_name": "GearshiftPosition", 79 | "bit_position": 41, 80 | "bit_size": 3, 81 | "decoder": "ignoreDecoder", 82 | "states": { 83 | "FOO": [1], 84 | "BAR": [2] 85 | } 86 | }, 87 | "StrAnglErr": { 88 | "decoder": "ignoreDecoder", 89 | "generic_name": "steering_wheel_angle_error", 90 | "bit_position": 44, 91 | "bit_size": 12 92 | }, 93 | "SomethingInvertedForcefully": { 94 | "generic_name": "inverted", 95 | "bit_numbering_inverted": true, 96 | "bit_position": 12, 97 | "bit_size": 8} 98 | }, 99 | "handlers": ["handleSteeringWheelMessage"] 100 | }, 101 | "0x49": { 102 | "name": "BrkSt", 103 | "bus": "hs", 104 | "max_frequency": 2, 105 | "signals": { 106 | "BrakePedalPosition": { 107 | "generic_name": "brake_pedal_status", 108 | "send_same": false, 109 | "decoder": "booleanDecoder", 110 | "bit_position": 0, 111 | "bit_size": 0 112 | } 113 | } 114 | }, 115 | "0x200": { 116 | "name": "MessageOnMS", 117 | "bus": "ms" 118 | }, 119 | "0x202": { 120 | "name": "InvertedMessage", 121 | "bus": "hs", 122 | "bit_numbering_inverted": true, 123 | "signals": { 124 | "InvertedOnMessage": { 125 | "generic_name": "inverted", 126 | "bit_position": 12, 127 | "bit_size": 8 128 | } 129 | } 130 | }, 131 | "0x121": { 132 | "name": "Cluster", 133 | "bus": "hs", 134 | "max_signal_frequency": 10, 135 | "force_send_changed_signals": true, 136 | "signals": { 137 | "TurnSignalLeft": { 138 | "generic_name": "turn_signal_left", 139 | "decoder": "booleanDecoder", 140 | "bit_position": 0, 141 | "bit_size": 0, 142 | "writable": true, 143 | "max_frequency": 5, 144 | "force_send_changed": false, 145 | "encoder": "booleanrncoder" 146 | }, 147 | "TurnSignalRight": { 148 | "generic_name": "turn_signal_right", 149 | "decoder": "booleanDecoder", 150 | "bit_position": 1, 151 | "bit_size": 0, 152 | "writable": true, 153 | "encoder": "booleanEncoder" 154 | } 155 | } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /tests/streamer_test_utils.py: -------------------------------------------------------------------------------- 1 | from nose.tools import eq_, ok_ 2 | 3 | class BaseStreamerTests(object): 4 | def test_receive_complete(self): 5 | serialized_message = self.streamer.serialize_for_stream({'name': "foo", 'value': 42}) 6 | ok_(len(serialized_message) > 1) 7 | self.streamer.receive(serialized_message) 8 | message = self.streamer.parse_next_message() 9 | ok_(message is not None) 10 | eq_(message['name'], "foo") 11 | eq_(message['value'], 42) 12 | 13 | def test_receive_partial(self): 14 | serialized_message = self.streamer.serialize_for_stream({'name': "foo", 'value': 42}) 15 | self.streamer.receive(serialized_message[:-5]) 16 | eq_(None, self.streamer.parse_next_message()) 17 | self.streamer.receive(serialized_message[-5:]) 18 | message = self.streamer.parse_next_message() 19 | ok_(message is not None) 20 | eq_(message['name'], "foo") 21 | eq_(message['value'], 42) 22 | 23 | def test_receive_two(self): 24 | serialized_messages = self.streamer.serialize_for_stream({'name': "foo", 'value': 42}) 25 | serialized_messages += self.streamer.serialize_for_stream({'name': "bar", 'value': 24}) 26 | self.streamer.receive(serialized_messages) 27 | message = self.streamer.parse_next_message() 28 | ok_(message is not None) 29 | eq_(message['name'], "foo") 30 | eq_(message['value'], 42) 31 | message = self.streamer.parse_next_message() 32 | ok_(message is not None) 33 | eq_(message['name'], "bar") 34 | eq_(message['value'], 24) 35 | eq_(None, self.streamer.parse_next_message()) 36 | 37 | def test_serialize_command(self): 38 | serialized_message = self.streamer.serialize_for_stream( 39 | {'command': "version"}) 40 | self.streamer.receive(serialized_message) 41 | message = self.streamer.parse_next_message() 42 | ok_(message is not None) 43 | eq_(message['command'], "version") 44 | 45 | class BaseFormatterTests(object): 46 | """A test for every format defined in the OpenXC Message Format 47 | spec: https://github.com/openxc/openxc-message-format 48 | """ 49 | 50 | def _check_serialized_deserialize_equal(self, deserialized): 51 | serialized = self.formatter.serialize(deserialized) 52 | eq_(deserialized, self.formatter.deserialize(serialized)) 53 | 54 | def test_simple_vehicle_message(self): 55 | self._check_serialized_deserialize_equal({'name': "foo", 'event': False, 'value': 42}) 56 | 57 | def test_command(self): 58 | self._check_serialized_deserialize_equal({'command': "version"}) 59 | self._check_serialized_deserialize_equal({'command': "device_id"}) 60 | 61 | def test_command_response(self): 62 | self._check_serialized_deserialize_equal({ "command_response": 63 | "version", "message": "v6.0-dev (default)", "status": True}) 64 | self._check_serialized_deserialize_equal({ "command_response": 65 | "device_id", "message": "v6.0-dev (default)", "status": True}) 66 | self._check_serialized_deserialize_equal({ "command_response": 67 | "passthrough", "status": False}) 68 | 69 | def test_passthrough_command(self): 70 | self._check_serialized_deserialize_equal({ "command": "passthrough", 71 | "bus": 1, 72 | "enabled": True 73 | }) 74 | 75 | def test_evented(self): 76 | self._check_serialized_deserialize_equal({"name": "button_event", 77 | "value": "up", "event": "pressed"}) 78 | 79 | def test_can_message(self): 80 | self._check_serialized_deserialize_equal({"bus": 1, "id": 1234, 81 | "data": "0x12345678"}) 82 | 83 | def test_diagnostic_request(self): 84 | self._check_serialized_deserialize_equal( 85 | { "command": "diagnostic_request", 86 | "action": "add", 87 | "request": { 88 | "bus": 1, 89 | "id": 1234, 90 | "mode": 1, 91 | "pid": 5, 92 | "payload": "0x1234", 93 | "multiple_responses": False, 94 | "frequency": 1, 95 | "name": "my_pid" 96 | } 97 | }) 98 | 99 | def test_diagnostic_response(self): 100 | self._check_serialized_deserialize_equal( 101 | {"bus": 1, 102 | "id": 1234, 103 | "mode": 1, 104 | "pid": 5, 105 | "frame": 0, 106 | "success": True, 107 | "payload": "0x1234" 108 | }) 109 | 110 | def test_negative_diagnostic_response(self): 111 | self._check_serialized_deserialize_equal({"bus": 1, 112 | "id": 1234, 113 | "pid": 0, 114 | "frame": 0, 115 | "mode": 1, 116 | "success": False, 117 | "negative_response_code": 17}) 118 | -------------------------------------------------------------------------------- /tests/test_binary_streamer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from .streamer_test_utils import BaseStreamerTests, BaseFormatterTests 4 | from openxc.formats.binary import ProtobufStreamer, ProtobufFormatter 5 | 6 | class ProtobufStreamerTests(unittest.TestCase, BaseStreamerTests): 7 | def setUp(self): 8 | super(ProtobufStreamerTests, self).setUp() 9 | self.streamer = ProtobufStreamer() 10 | 11 | 12 | class ProtobufFormatterTests(unittest.TestCase, BaseFormatterTests): 13 | def setUp(self): 14 | super(ProtobufFormatterTests, self).setUp() 15 | self.formatter = ProtobufFormatter 16 | -------------------------------------------------------------------------------- /tests/test_filerecorder.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openxc.sinks import FileRecorderSink 4 | 5 | class FileRecorderSinkTest(unittest.TestCase): 6 | 7 | def test_create(self): 8 | FileRecorderSink() 9 | -------------------------------------------------------------------------------- /tests/test_generate_diagnostic.py: -------------------------------------------------------------------------------- 1 | from nose.tools import ok_, eq_ 2 | import unittest 3 | import os 4 | 5 | from openxc.generator.message_sets import JsonMessageSet 6 | from openxc.generator.structures import BitInversionError, Signal 7 | from openxc.generator.coder import CodeGenerator 8 | 9 | class DiagnosticCodeGeneratorTests(unittest.TestCase): 10 | 11 | def _generate(self, filename): 12 | search_paths = [os.path.dirname(__file__)] 13 | generator = CodeGenerator(search_paths) 14 | 15 | message_set = JsonMessageSet.parse( 16 | os.path.join(os.path.dirname(__file__), filename), 17 | search_paths=search_paths, 18 | skip_disabled_mappings=True) 19 | ok_(message_set.validate()) 20 | 21 | generator.message_sets.append(message_set) 22 | return message_set, generator.build_source() 23 | 24 | def _validate(self, filename): 25 | message_set, output = self._generate(filename) 26 | for request in message_set.active_diagnostic_messages(): 27 | ok_(("0x%x" % request.id) in output) 28 | if request.decoder is not None: 29 | ok_(request.decoder in output) 30 | 31 | def test_includes_decoder(self): 32 | self._validate('diagnostic.json.example') 33 | 34 | def test_includes_diagnostics(self): 35 | message_set, output = self._generate('signals.json.example') 36 | ok_(len(list(message_set.all_diagnostic_messages())) > 0) 37 | for diagnostic_request in message_set.all_diagnostic_messages(): 38 | ok_(diagnostic_request.name in output) 39 | -------------------------------------------------------------------------------- /tests/test_generator.py: -------------------------------------------------------------------------------- 1 | from nose.tools import ok_, eq_ 2 | import unittest 3 | import os 4 | 5 | from openxc.generator.message_sets import JsonMessageSet 6 | from openxc.generator.structures import BitInversionError, Signal 7 | from openxc.generator.coder import CodeGenerator 8 | 9 | class CodeGeneratorTests(unittest.TestCase): 10 | 11 | def _generate(self, filename): 12 | search_paths = [os.path.dirname(__file__)] 13 | generator = CodeGenerator(search_paths) 14 | 15 | message_set = JsonMessageSet.parse( 16 | os.path.join(os.path.dirname(__file__), filename), 17 | search_paths=search_paths, 18 | skip_disabled_mappings=True) 19 | ok_(message_set.validate()) 20 | 21 | generator.message_sets.append(message_set) 22 | return message_set, generator.build_source() 23 | 24 | def _validate(self, filename): 25 | message_set, output = self._generate(filename) 26 | for signal in message_set.enabled_signals(): 27 | ok_(signal.generic_name in output) 28 | 29 | for message in message_set.active_messages(): 30 | ok_(message.name in output) 31 | ok_(("0x%x" % message.id) in output) 32 | 33 | def test_ignore_flag(self): 34 | message_set, output = self._generate('signals.json.example') 35 | for signal in message_set.enabled_signals(): 36 | if signal.ignore: 37 | eq_(output.count(signal.name), 1) 38 | 39 | def test_ignore_with_states(self): 40 | message_set, output = self._generate('signals.json.example') 41 | for line in output.split("\n"): 42 | if 'IgnoredPos' in line and 'GearshiftPosition' in line: 43 | ok_('ignoreDecoder' in line) 44 | if 'GrshftPos' in line and 'GearshiftPosition' in line: 45 | ok_('stateDecoder' in line) 46 | 47 | def test_non_mapped(self): 48 | self._validate('signals.json.example') 49 | 50 | def test_mapped(self): 51 | self._validate('mapped_signal_set.json.example') 52 | 53 | def test_raw_can_mode(self): 54 | message_set, output = self._generate('signals.json.example') 55 | eq_(list(message_set.valid_buses())[0].raw_can_mode, "filtered") 56 | eq_(list(message_set.valid_buses())[1].raw_can_mode, "off") 57 | eq_(output.count("bypassFilters: false"), 2) 58 | eq_(output.count("0x200"), 1) 59 | eq_(output.count("passthroughCanMessages: true"), 1) 60 | 61 | def test_max_message_frequency(self): 62 | message_set, output = self._generate('signals.json.example') 63 | eq_(list(message_set.valid_buses())[0].max_message_frequency, 5) 64 | eq_(list(message_set.valid_buses())[1].max_message_frequency, 0) 65 | 66 | def test_unfiltered_raw_can_mode(self): 67 | message_set, output = self._generate('mapped_signal_set.json.example') 68 | eq_(list(message_set.valid_buses())[0].raw_can_mode, "off") 69 | eq_(list(message_set.valid_buses())[1].raw_can_mode, "unfiltered") 70 | eq_(output.count("passthroughCanMessages: true"), 1) 71 | eq_(output.count("bypassFilters: true"), 1) 72 | 73 | def test_bit_inversion(self): 74 | 75 | eq_(Signal._invert_bit_index(24, 16), 16) 76 | eq_(Signal._invert_bit_index(48, 1), 55) 77 | eq_(Signal._invert_bit_index(8, 16), 0) 78 | 79 | def test_invalid_bit_inversion(self): 80 | try: 81 | Signal._invert_bit_index(0, 16) 82 | except BitInversionError: 83 | pass 84 | else: 85 | self.fail("") 86 | 87 | def test_max_frequency_on_message_cascades(self): 88 | message_set, output = self._generate('signals.json.example') 89 | message_with_frequency = [ 90 | message for message in message_set.all_messages() 91 | if message.id == 0x121][0] 92 | eq_(message_with_frequency.max_signal_frequency, 10) 93 | 94 | signal_with_cascaded_frequency = message_with_frequency.signals['TurnSignalRight'] 95 | eq_(signal_with_cascaded_frequency.max_frequency, 10) 96 | 97 | signal_with_overridden_frequency = message_with_frequency.signals['TurnSignalLeft'] 98 | eq_(signal_with_overridden_frequency.max_frequency, 5) 99 | 100 | def test_default_max_frequency(self): 101 | message_set, output = self._generate('mapped_signal_set.json.example') 102 | message = [message for message in message_set.all_messages() 103 | if message.id == 0x128][0] 104 | eq_(message.max_frequency, 0) 105 | 106 | def test_max_frequency_on_set_cascades(self): 107 | message_set, output = self._generate('signals.json.example') 108 | message = [message for message in message_set.all_messages() 109 | if message.id == 0x128][0] 110 | eq_(message.max_frequency, 5) 111 | 112 | def test_override_max_frequency_on_bus(self): 113 | message_set, output = self._generate('signals.json.example') 114 | message = [message for message in message_set.all_messages() 115 | if message.id == 0x49][0] 116 | eq_(message.max_frequency, 2) 117 | 118 | def test_max_frequency_on_bus_cascades(self): 119 | message_set, output = self._generate('signals.json.example') 120 | message = [message for message in message_set.all_messages() 121 | if message.id == 0x200][0] 122 | eq_(message.max_frequency, 0) 123 | 124 | def test_message_frequency_in_output(self): 125 | message_set, output = self._generate('signals.json.example') 126 | message_with_frequency = [ 127 | message for message in message_set.all_messages() 128 | if message.id == 0x121][0] 129 | 130 | found = False 131 | for line in output.split("\n"): 132 | if "CAN_BUSES" in line and message_with_frequency.name in line: 133 | ok_(("{%f}" % message_with_frequency.max_frequency) in line, 134 | "Frequency %f should be in output: %s" % 135 | (message_with_frequency.max_frequency, line)) 136 | found = True 137 | ok_(found) 138 | 139 | def test_force_send_on_message_cascades(self): 140 | message_set, output = self._generate('signals.json.example') 141 | message = [ 142 | message for message in message_set.all_messages() 143 | if message.id == 0x121][0] 144 | ok_(message.force_send_changed_signals) 145 | 146 | signal_with_overridden_flag = message.signals['TurnSignalLeft'] 147 | ok_(not signal_with_overridden_flag.force_send_changed) 148 | 149 | signal_with_cascaded_flag = message.signals['TurnSignalRight'] 150 | ok_(signal_with_cascaded_flag.force_send_changed) 151 | 152 | def test_default_force_send(self): 153 | message_set, output = self._generate('signals.json.example') 154 | message = [message for message in message_set.all_messages() 155 | if message.id == 0x128][0] 156 | ok_(not message.force_send_changed_signals) 157 | 158 | def test_force_send_in_output(self): 159 | message_set, output = self._generate('signals.json.example') 160 | message = [message for message in message_set.all_messages() 161 | if message.id == 0x121][0] 162 | 163 | found = False 164 | for line in output.split("\n"): 165 | if "CAN_BUSES" in line and message.name in line: 166 | # this isn't a very particular test, but it works for now 167 | ok_(("%s" % str(message.force_send_changed_signals).lower()) in line) 168 | found = True 169 | ok_(found) 170 | 171 | def test_bit_numbering_default(self): 172 | message_set, output = self._generate('signals.json.example') 173 | message = [message for message in message_set.all_messages() 174 | if message.id == 0x128][0] 175 | signal = message.signals['StrAnglErr'] 176 | ok_(not signal.bit_numbering_inverted) 177 | ok_(signal.bit_position == 44) 178 | 179 | for message in message_set.all_messages(): 180 | if message.id != 0x202: 181 | ok_(not message.bit_numbering_inverted) 182 | 183 | def test_bit_numbering_override(self): 184 | message_set, output = self._generate('signals.json.example') 185 | message = [message for message in message_set.all_messages() 186 | if message.id == 0x128][0] 187 | signal = message.signals['SomethingInvertedForcefully'] 188 | ok_(signal.bit_numbering_inverted) 189 | ok_(signal.bit_position != 12) 190 | 191 | def test_bit_numbering_on_message(self): 192 | message_set, output = self._generate('signals.json.example') 193 | message = [message for message in message_set.all_messages() 194 | if message.id == 0x202][0] 195 | signal = message.signals['InvertedOnMessage'] 196 | ok_(signal.bit_numbering_inverted) 197 | ok_(signal.bit_position != 12) 198 | 199 | def test_includes_diagnostics(self): 200 | message_set, output = self._generate('signals.json.example') 201 | ok_(len(list(message_set.all_diagnostic_messages())) > 0) 202 | for diagnostic_request in message_set.all_diagnostic_messages(): 203 | ok_(diagnostic_request.name in output) 204 | -------------------------------------------------------------------------------- /tests/test_json_streamer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from .streamer_test_utils import BaseStreamerTests, BaseFormatterTests 4 | from openxc.formats.json import JsonStreamer, JsonFormatter 5 | 6 | class JsonStreamerTests(unittest.TestCase, BaseStreamerTests): 7 | def setUp(self): 8 | super(JsonStreamerTests, self).setUp() 9 | self.streamer = JsonStreamer() 10 | 11 | class JsonFormatterTests(unittest.TestCase, BaseFormatterTests): 12 | def setUp(self): 13 | super(JsonFormatterTests, self).setUp() 14 | self.formatter = JsonFormatter 15 | -------------------------------------------------------------------------------- /tests/test_measurement.py: -------------------------------------------------------------------------------- 1 | from nose.tools import eq_, ok_ 2 | import unittest 3 | import time 4 | 5 | from openxc.measurements import Measurement, VehicleSpeed 6 | 7 | class MeasurementTests(unittest.TestCase): 8 | def setUp(self): 9 | super(MeasurementTests, self).setUp() 10 | 11 | def test_basic(self): 12 | Measurement("name", "value", override_unit=True) 13 | 14 | def test_has_age(self): 15 | measurement = Measurement("name", "value", override_unit=True) 16 | age = measurement.age 17 | time.sleep(0.001) 18 | ok_(measurement.age > age) 19 | 20 | def test_unit(self): 21 | measurement = VehicleSpeed(42, override_unit=True) 22 | try: 23 | eq_(measurement.value, 42) 24 | except AttributeError: 25 | pass 26 | else: 27 | self.fail() 28 | eq_(measurement.value, measurement.unit(42)) 29 | 30 | def test_override_unit(self): 31 | try: 32 | VehicleSpeed(42) 33 | except AttributeError: 34 | pass 35 | else: 36 | self.fail() 37 | 38 | VehicleSpeed(42, override_unit=True) 39 | 40 | def test_assign_value(self): 41 | measurement = VehicleSpeed(42, override_unit=True) 42 | new_value = VehicleSpeed.unit(42) 43 | 44 | try: 45 | measurement.value = 24 46 | except AttributeError: 47 | eq_(measurement.value, new_value) 48 | else: 49 | self.fail() 50 | 51 | measurement.value = new_value 52 | eq_(measurement.value, new_value) 53 | -------------------------------------------------------------------------------- /tests/test_network.py: -------------------------------------------------------------------------------- 1 | from nose.tools import eq_, ok_ 2 | import unittest 3 | 4 | import openxc.measurements 5 | from openxc.sources import NetworkDataSource 6 | from openxc.sources import DataSourceError 7 | 8 | class NetworkDataSourceTests(unittest.TestCase): 9 | def setUp(self): 10 | super(NetworkDataSourceTests, self).setUp() 11 | 12 | def test_create(self): 13 | def callback(message): 14 | pass 15 | 16 | try: 17 | s = NetworkDataSource(callback=callback, host='localhost') 18 | except DataSourceError as e: 19 | pass 20 | -------------------------------------------------------------------------------- /tests/test_serial.py: -------------------------------------------------------------------------------- 1 | from nose.tools import eq_, ok_ 2 | import unittest 3 | 4 | import openxc.measurements 5 | from openxc.sources import SerialDataSource 6 | from openxc.sources import DataSourceError 7 | 8 | class SerialDataSourceTests(unittest.TestCase): 9 | def setUp(self): 10 | super(SerialDataSourceTests, self).setUp() 11 | 12 | def test_create(self): 13 | def callback(message): 14 | pass 15 | 16 | try: 17 | s = SerialDataSource(callback=callback) 18 | except DataSourceError as e: 19 | pass 20 | -------------------------------------------------------------------------------- /tests/test_trace.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from nose.tools import ok_ 3 | 4 | from openxc.sources import TraceDataSource 5 | 6 | class TraceDataSourceTests(unittest.TestCase): 7 | def _receive(self, message, **kwargs): 8 | self.received = True 9 | 10 | def test_without_timestamp(self): 11 | self.source = TraceDataSource(filename="tests/trace-no-timestamp.json", 12 | callback=self._receive, loop=False) 13 | self.received = False 14 | self.source.start() 15 | self.source.join() 16 | ok_(self.received) 17 | 18 | def test_playback(self): 19 | self.source = TraceDataSource(filename="tests/trace.json", 20 | callback=self._receive, loop=False) 21 | self.received = False 22 | self.source.start() 23 | self.source.join() 24 | ok_(self.received) 25 | -------------------------------------------------------------------------------- /tests/test_uploader.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openxc.sinks import UploaderSink 4 | 5 | class UploaderSinkTest(unittest.TestCase): 6 | 7 | def test_create(self): 8 | UploaderSink("http://openxcplatform.com") 9 | -------------------------------------------------------------------------------- /tests/test_usb.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openxc.sources import UsbDataSource, DataSourceError 4 | 5 | class UsbDataSourceTests(unittest.TestCase): 6 | def setUp(self): 7 | super(UsbDataSourceTests, self).setUp() 8 | 9 | def test_create(self): 10 | def callback(message): 11 | pass 12 | -------------------------------------------------------------------------------- /tests/test_vehicle.py: -------------------------------------------------------------------------------- 1 | from nose.tools import eq_, ok_ 2 | import unittest 3 | import time 4 | 5 | from openxc.sources import DataSource 6 | from openxc.measurements import Measurement, NamedMeasurement, \ 7 | UnrecognizedMeasurementError 8 | from openxc.vehicle import Vehicle 9 | 10 | 11 | class VehicleTests(unittest.TestCase): 12 | def setUp(self): 13 | super(VehicleTests, self).setUp() 14 | self.vehicle = Vehicle() 15 | self.messages_received = [] 16 | 17 | def _callback(self, message): 18 | self.messages_received.append(message) 19 | 20 | def test_get(self): 21 | measurement = self.vehicle.get(TestMeasurement) 22 | ok_(measurement is None) 23 | 24 | def test_add_listener(self): 25 | source = TestDataSource() 26 | self.vehicle.add_source(source) 27 | 28 | self.vehicle.listen(TestMeasurement, self._callback) 29 | data = {'name': TestMeasurement.name, 'value': 100} 30 | source.inject(data) 31 | ok_(len(self.messages_received) > 0) 32 | 33 | def test_remove_listener(self): 34 | source = TestDataSource() 35 | self.vehicle.add_source(source) 36 | 37 | self.vehicle.listen(TestMeasurement, self._callback) 38 | self.vehicle.unlisten(TestMeasurement, self._callback) 39 | data = {'name': TestMeasurement.name, 'value': 100} 40 | source.inject(data) 41 | eq_(len(self.messages_received), 0) 42 | 43 | def test_get_one(self): 44 | source = TestDataSource() 45 | self.vehicle.add_source(source) 46 | measurement = self.vehicle.get(TestMeasurement) 47 | ok_(measurement is None) 48 | 49 | data = {'name': TestMeasurement.name, 'value': 100} 50 | source.inject(data) 51 | measurement = self.vehicle.get(TestMeasurement) 52 | ok_(measurement is not None) 53 | eq_(measurement.name, data['name']) 54 | eq_(measurement.value.num, data['value']) 55 | 56 | def test_bad_measurement_type(self): 57 | class NotAMeasurement(object): 58 | pass 59 | 60 | try: 61 | self.vehicle.get(NotAMeasurement) 62 | except UnrecognizedMeasurementError: 63 | pass 64 | else: 65 | self.fail("Expected an %s" % 66 | str(UnrecognizedMeasurementError.__name__)) 67 | 68 | 69 | class TestMeasurement(NamedMeasurement): 70 | name = "test" 71 | 72 | 73 | class TestDataSource(DataSource): 74 | def inject(self, message): 75 | self.callback(message) 76 | time.sleep(0.001) 77 | 78 | def run(self): 79 | pass 80 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | from nose.tools import eq_ 2 | 3 | import openxc.version 4 | 5 | 6 | def test_get_version(): 7 | version = openxc.version.get_version() 8 | eq_(type(version), str) 9 | -------------------------------------------------------------------------------- /tests/trace-no-timestamp.json: -------------------------------------------------------------------------------- 1 | {"name":"accelerator_pedal_position","value":0} 2 | {"name":"engine_speed","value":772} 3 | {"name":"vehicle_speed","value":0} 4 | {"name":"accelerator_pedal_position","value":0} 5 | {"name":"torque_at_transmission","value":3} 6 | {"name":"engine_speed","value":772} 7 | {"name":"vehicle_speed","value":0} 8 | {"name":"accelerator_pedal_position","value":0} 9 | {"name":"engine_speed","value":772} 10 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py27 8 | 9 | [testenv] 10 | commands = {envpython} setup.py test 11 | --------------------------------------------------------------------------------