diff --git a/.appveyor.yml b/.appveyor.yml index 56c97233d..ee7b98300 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -3,29 +3,31 @@ environment: # For Python versions available on Appveyor, see # http://www.appveyor.com/docs/installed-software#python + # Python 3.0-3.3 have reached EOL - PYTHON: "C:\\Python27" - PYTHON: "C:\\Python34" - PYTHON: "C:\\Python35" - PYTHON: "C:\\Python36" + - PYTHON: "C:\\Python37" - PYTHON: "C:\\Python27-x64" - PYTHON: "C:\\Python34-x64" - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36-x64" - - # Python 3.3 has reached EOL + - PYTHON: "C:\\Python37-x64" install: - # Prepend Python installation to PATH - - set PATH=%PYTHON_INSTALL%;%PATH% - - # Prepend Python scripts to PATH (e.g. py.test) - - set PATH=%PYTHON_INSTALL%\\Scripts;%PATH% + # Prepend Python installation and scripts (e.g. pytest) to PATH + - set PATH=%PYTHON_INSTALL%;%PYTHON_INSTALL%\\Scripts;%PATH% - # We need to install the python-can library itself - - "python -m pip install .[test]" + # We need to install the python-can library itself including the dependencies + - "python -m pip install .[test,neovi]" build: off test_script: - - "pytest -v --timeout=300" + # run tests + - "pytest" + + # uplad coverage reports + - "codecov" diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 000000000..16168f521 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,21 @@ +# Validate with curl --data-binary @.codecov.yml https://codecov.io/validate +codecov: + archive: + uploads: no + +coverage: + precision: 2 + round: down + range: 50...100 + status: + project: + default: + # coverage may fall by <1.0% and still be considered "passing" + threshold: 1.0% + patch: + default: + # coverage may fall by <1.0% and still be considered "passing" + threshold: 1.0% + +comment: + layout: "header, diff, changes" diff --git a/.gitignore b/.gitignore index 96acb31a4..6b813427e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ -# https://github.com/github/gitignore/blob/da00310ccba9de9a988cc973ef5238ad2c1460e9/Python.gitignore +test/__tempdir__/ +.pytest_cache/ + +# ------------------------- +# below: https://github.com/github/gitignore/blob/da00310ccba9de9a988cc973ef5238ad2c1460e9/Python.gitignore # Byte-compiled / optimized / DLL files __pycache__/ @@ -11,6 +15,7 @@ __pycache__/ # Distribution / packaging .Python env/ +venv/ build/ develop-eggs/ dist/ diff --git a/.travis.yml b/.travis.yml index 358d32666..5f8204b29 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,16 @@ language: python python: - # CPython: + # CPython; versions 3.0-3.3 have reached EOL - "2.7" - # Python 3.3 has reached EOL and pytest fails there - "3.4" - "3.5" - "3.6" - "3.7-dev" # TODO: change to "3.7" once it is supported by travis-ci - "nightly" # PyPy: - - "pypy" - - "pypy3.5" + - "pypy" # Python 2.7 + - "pypy3.5" # Python 3.5 os: - linux # Linux is officially supported and we test the library under @@ -19,38 +18,45 @@ os: # - osx # OSX + Python is not officially supported by Travis CI as of Feb. 2018 # nevertheless, "nightly" and some "*-dev" versions seem to work, so we - # include them explicitly below (see "matrix: include: ..." below) + # include them explicitly below (see "matrix: include: ..." below). + # They only seem to work with the xcode8.3 image, and not the newer ones. + # Thus we will leave this in, until it breaks one day, at which point we + # will probably reomve testing on OSX if it is not supported then. + # See #385 on Github. # - windows # Windows is not supported at all by Travis CI as of Feb. 2018 # Linux setup dist: trusty -sudo: false +sudo: required matrix: # see "os: ..." above include: - os: osx + osx_image: xcode8.3 python: "3.6-dev" - os: osx + osx_image: xcode8.3 python: "3.7-dev" - os: osx + osx_image: xcode8.3 python: "nightly" allow_failures: # allow all nighly builds to fail, since these python versions might be unstable - python: "nightly" - # we do not allow dev builds to fail, since these builds are considered stable enough install: - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo bash test/open_vcan.sh ; fi + - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then travis_retry pip install -r doc/doc-requirements.txt; fi - travis_retry pip install .[test] - - travis_retry pip install sphinx script: - - pytest -v --timeout=300 + - pytest + - codecov # Build Docs with Sphinx - # # -a Write all files - - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then python -m sphinx -an doc build; fi \ No newline at end of file + # -n nitpicky + - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then python -m sphinx -an doc build; fi diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d56a151de..ef0d76c17 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,108 @@ +Version 3.0.0 +==== + +Major features +-------------- + +* Adds support for developing `asyncio` applications with `python-can` more easily. This can be useful + when implementing protocols that handles simultaneous connections to many nodes since you can write + synchronous looking code without handling multiple threads and locking mechanisms. #388 +* New can viewer terminal application. #390 +* More formally adds task management responsibility to the `Bus`. By default tasks created with + `bus.send_periodic` will have a reference held by the bus - this means in many cases the user + doesn't need to keep the task in scope for their periodic messages to continue being sent. If + this behavior isn't desired pass `store_task=False` to the `send_periodic` method. Stop all tasks + by calling the bus's new `stop_all_periodic_tasks` method. #412 + + +Breaking changes +---------------- + +- Interfaces should no longer override `send_periodic` and instead implement + `_send_periodic_internal` #426 +- writing to closed writers is not supported any more (it was supported only for some) +- the method `Listener.on_message_received()` is now abstract (using `@abc.abstractmethod`) +- the file in the reader/writer is now always stored in the attribute uniformly called `file`, and not in + something like `fp`, `log_file` or `output_file`. Changed the name of the first parameter of the + read/writer constructors from `filename` to `file`. + + +Other notable changes +--------------------- + +* can.Message class updated #413 + - Addition of a Message.equals method. + - Deprecate id_type in favor of is_extended_id + - documentation, testing and example updates + - Addition of support for various builtins: __repr__, __slots__, __copy__ +* IO module updates to bring consistency to the different CAN message writers and readers. #348 + - context manager support for all readers and writers + - they share a common super class called `BaseIOHandler` + - all file handles can now be closed with the `stop()` method + - the table name in `SqliteReader`/`SqliteWriter` can be adjusted + - append mode added in `CSVWriter` and `CanutilsLogWriter` + - [file-like](https://docs.python.org/3/glossary.html#term-file-like-object) and + [path-like](https://docs.python.org/3/glossary.html#term-path-like-object) objects can now be passed to + the readers and writers (except to the Sqlite handlers) + - add a `__ne__()` method to the `Message` class (this was required by the tests) + - added a `stop()` method for `BufferedReader` + - `SqliteWriter`: this now guarantees that all messages are being written, exposes some previously internal metrics + and only buffers messages up to a certain limit before writing/committing to the database. + - the unused `header_line` attribute from `CSVReader` has been removed + - privatized some attributes that are only to be used internally in the classes +* Start testing against Python 3.7 #380 +* All scripts have been moved into `can/scripts`. #370, #406 +* Added support for additional sections to the config #338 +* Code coverage reports added. #346, #374 +* Bug fix to thread safe bus. #397 + +General fixes, cleanup and docs changes: (#347, #348, #367, #368, #370, #371, #373, #420, #417, #419) + +Backend Specific Changes +------------------------ + +3rd party interfaces +~~~~~~~~~~~~~~~~~~~~ + +* Deprecated `python_can.interface` entry point instead use `can.interface`. #389 + +neovi +~~~~~ + +* Added support for CAN-FD #408 +* Fix issues checking if bus is open. #381 +* Adding multiple channels support. #415 + +nican +~~~~~ + +* implements reset instead of custom `flush_tx_buffer`. #364 + +pcan +~~~~ + +* now supported on OSX. #365 + + +serial +~~~~~~ + +* Removed TextIOWrapper from serial. #383 +* switch to `serial_for_url` enabling using remote ports via `loop://`, ``socket://` and `rfc2217://` URLs. #393 +* hardware handshake using `rtscts` kwarg #402 + +socketcan +~~~~~~~~~ + +* socketcan tasks now reuse a bcm socket +* socketcan bugfix to receive error frames #384 + +vector +~~~~~~ + +* Vector interface now implements `_detect_available_configs`. #362 +* Added support to select device by serial number. #387 + Version 2.2.1 (2018-07-12) ===== @@ -14,7 +119,7 @@ Version 2.2.0 (2018-06-30) * Added synchronized (thread-safe) Bus variant. * context manager support for the Bus class. * Dropped support for Python 3.3 (officially reached end-of-life in Sept. 2017) -* Deprecated the old `CAN` module, please use the newer `can` entry point (will be removed in version 2.4) +* Deprecated the old `CAN` module, please use the newer `can` entry point (will be removed in an upcoming major version) Version 2.1.0 (2018-02-17) ===== @@ -34,7 +139,7 @@ Version 2.0.0 (2018-01-05 After an extended baking period we have finally tagged version 2.0.0! -Quite a few major Changes from v1.x: +Quite a few major changes from v1.x: * New interfaces: * Vector diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 5e5ea882b..0576c2b66 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -1,23 +1,24 @@ -Ben Powell -Brian Thorne -Geert Linders -Mark Catley -Phillip Dixon -Rose Lu -Karl van Workum -Albert Bloomfield -Sam Bristow -Ethan Zonca -Robert Kaye -Andrew Beal -Jonas Frid -Tynan McAuley -Bruno Pennati -Jack Jester-Weinstein -Joshua Villyard -Giuseppe Corbelli -Christian Sandberg -Eduard Bröcker -Boris Wenzlaff -Pierre-Luc Tessier Gagné -Felix Divo +Ben Powell +Brian Thorne +Geert Linders +Mark Catley +Phillip Dixon +Rose Lu +Karl van Workum +Albert Bloomfield +Sam Bristow +Ethan Zonca +Robert Kaye +Andrew Beal +Jonas Frid +Tynan McAuley +Bruno Pennati +Jack Jester-Weinstein +Joshua Villyard +Giuseppe Corbelli +Christian Sandberg +Eduard Bröcker +Boris Wenzlaff +Pierre-Luc Tessier Gagné +Felix Divo +Kristian Sloth Lauszus diff --git a/LICENSE.txt b/LICENSE.txt index b14ca0a55..65c5ca88a 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,165 +1,165 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/MANIFEST.in b/MANIFEST.in index 05cd56c0d..4079706c7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include *.txt +include test/data/*.* recursive-include doc *.rst diff --git a/README.rst b/README.rst index 8e9ad8b7d..cf6b45f83 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ python-can ========== -|release| |docs| |build_travis| |build_appveyor| +|release| |docs| |build_travis| |build_appveyor| |coverage| .. |release| image:: https://img.shields.io/pypi/v/python-can.svg :target: https://pypi.python.org/pypi/python-can/ @@ -9,8 +9,8 @@ python-can .. |docs| image:: https://readthedocs.org/projects/python-can/badge/?version=stable :target: https://python-can.readthedocs.io/en/stable/ - :alt: Documentation build Status - + :alt: Documentation + .. |build_travis| image:: https://travis-ci.org/hardbyte/python-can.svg?branch=develop :target: https://travis-ci.org/hardbyte/python-can/branches :alt: Travis CI Server for develop branch @@ -19,19 +19,65 @@ python-can :target: https://ci.appveyor.com/project/hardbyte/python-can/history :alt: AppVeyor CI Server for develop branch +.. |coverage| image:: https://codecov.io/gh/hardbyte/python-can/branch/develop/graph/badge.svg + :target: https://codecov.io/gh/hardbyte/python-can/branch/develop + :alt: Test coverage reports on Codecov.io + The **C**\ ontroller **A**\ rea **N**\ etwork is a bus standard designed to allow microcontrollers and devices to communicate with each other. It -has priority based bus arbitration, reliable deterministic +has priority based bus arbitration and reliable deterministic communication. It is used in cars, trucks, boats, wheelchairs and more. The ``can`` package provides controller area network support for -Python developers; providing `common abstractions to -different hardware devices`, and a suite of utilities for sending and receiving +Python developers; providing common abstractions to +different hardware devices, and a suite of utilities for sending and receiving messages on a can bus. The library supports Python 2.7, Python 3.4+ as well as PyPy 2 & 3 and runs on Mac, Linux and Windows. + +Features +-------- + +- common abstractions for CAN communication +- support for many different backends (see the `docs `__) +- receiving, sending, and periodically sending messages +- normal and extended arbitration IDs +- limited `CAN FD `__ support +- many different loggers and readers supporting playback: ASC (CANalyzer format), BLF (Binary Logging Format by Vector), CSV, SQLite and Canutils log +- efficient in-kernel or in-hardware filtering of messages on supported interfaces +- bus configuration reading from file or environment variables +- CLI tools for working with CAN busses (see the `docs `__) +- more + + +Example usage +------------- + +.. code:: python + + # import the library + import can + + # create a bus instance + # many other interfaces are supported as well (see below) + bus = can.Bus(interface='socketcan', + channel='vcan0', + receive_own_messages=True) + + # send a message + message = can.Message(arbitration_id=123, extended_id=True, + data=[0x11, 0x22, 0x33]) + bus.send(message, timeout=0.2) + + # iterate over received messages + for msg in bus: + print("{X}: {}".format(msg.arbitration_id, msg.data)) + + # or use an asynchronous notifier + notifier = can.Notifier(bus, [can.Logger("recorded.log"), can.Printer()]) + You can find more information in the documentation, online at `python-can.readthedocs.org `__. @@ -40,7 +86,7 @@ Discussion ---------- If you run into bugs, you can file them in our -`issue tracker `__. +`issue tracker `__ on GitHub. There is also a `python-can `__ mailing list for development discussion. @@ -51,6 +97,7 @@ questions and answers tagged with ``python+can``. Wherever we interact, we strive to follow the `Python Community Code of Conduct `__. + Contributing ------------ diff --git a/can/CAN.py b/can/CAN.py index fde1cf3d6..0ed96dfb1 100644 --- a/can/CAN.py +++ b/can/CAN.py @@ -1,15 +1,14 @@ -#!/usr/bin/env python # coding: utf-8 """ This module was once the core of python-can, containing implementations of all the major classes in the library, now however all functionality has been refactored out. This API -is left intact for version 2.0 to 2.3 to aide with migration. +is left intact for version 2.x to aide with migration. WARNING: -This module is deprecated an will get removed in version 2.4. -Please use `import can` instead. +This module is deprecated an will get removed in version 3.x. +Please use ``import can`` instead. """ from __future__ import absolute_import @@ -19,14 +18,12 @@ from can.util import set_logging_level from can.io import * -import logging +import warnings -log = logging.getLogger('can') - -# See #267 +# See #267. # Version 2.0 - 2.1: Log a Debug message # Version 2.2: Log a Warning -# Version 2.3: Log an Error -# Version 2.4: Remove the module -log.warning('Loading python-can via the old "CAN" API is deprecated since v2.0 an will get removed in v2.4. ' - 'Please use `import can` instead.') +# Version 3.x: DeprecationWarning +# Version 4.0: Remove the module +warnings.warn('Loading python-can via the old "CAN" API is deprecated since v3.0 an will get removed in v4.0 ' + 'Please use `import can` instead.', DeprecationWarning) diff --git a/can/__init__.py b/can/__init__.py index 42c20d2b8..2c973c764 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -9,7 +9,7 @@ import logging -__version__ = "2.2.1" +__version__ = "3.0.0-dev" log = logging.getLogger('can') @@ -23,6 +23,10 @@ class CanError(IOError): pass from .listener import Listener, BufferedReader, RedirectReader +try: + from .listener import AsyncBufferedReader +except ImportError: + pass from .io import Logger, Printer, LogReader, MessageSync from .io import ASCWriter, ASCReader @@ -34,14 +38,14 @@ class CanError(IOError): from .util import set_logging_level from .message import Message -from .bus import BusABC +from .bus import BusABC, BusState from .thread_safe_bus import ThreadSafeBus from .notifier import Notifier from .interfaces import VALID_INTERFACES from . import interface from .interface import Bus, detect_available_configs -from can.broadcastmanager import send_periodic, \ +from .broadcastmanager import send_periodic, \ CyclicSendTaskABC, \ LimitedDurationCyclicSendTaskABC, \ ModifiableCyclicTaskABC, \ diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 2c7072739..6837f0e67 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -10,22 +9,24 @@ import abc import logging - import threading import time - +import warnings log = logging.getLogger('can.bcm') class CyclicTask(object): """ - Abstract Base for all Cyclic Tasks + Abstract Base for all cyclic tasks. """ @abc.abstractmethod def stop(self): """Cancel this periodic task. + + :raises can.CanError: + If stop is called on an already stopped task. """ @@ -36,7 +37,7 @@ class CyclicSendTaskABC(CyclicTask): def __init__(self, message, period): """ - :param message: The :class:`can.Message` to be sent periodically. + :param can.Message message: The message to be sent periodically. :param float period: The rate in seconds at which to send the message. """ self.message = message @@ -51,7 +52,7 @@ class LimitedDurationCyclicSendTaskABC(CyclicSendTaskABC): def __init__(self, message, period, duration): """Message send task with a defined duration and period. - :param message: The :class:`can.Message` to be sent periodically. + :param can.Message message: The message to be sent periodically. :param float period: The rate in seconds at which to send the message. :param float duration: The duration to keep sending this message at given rate. @@ -76,19 +77,28 @@ def modify_data(self, message): """Update the contents of this periodically sent message without altering the timing. - :param message: The :class:`~can.Message` with new :attr:`can.Message.data`. + :param can.Message message: + The message with the new :attr:`can.Message.data`. + Note: The arbitration ID cannot be changed. """ self.message = message class MultiRateCyclicSendTaskABC(CyclicSendTaskABC): """Exposes more of the full power of the TX_SETUP opcode. - - Transmits a message `count` times at `initial_period` then - continues to transmit message at `subsequent_period`. """ def __init__(self, channel, message, count, initial_period, subsequent_period): + """ + Transmits a message `count` times at `initial_period` then continues to + transmit message at `subsequent_period`. + + :param can.interface.Bus channel: + :param can.Message message: + :param int count: + :param float initial_period: + :param float subsequent_period: + """ super(MultiRateCyclicSendTaskABC, self).__init__(channel, message, subsequent_period) @@ -135,12 +145,14 @@ def _run(self): def send_periodic(bus, message, period, *args, **kwargs): - """Send a message every `period` seconds on the given channel. + """ + Send a :class:`~can.Message` every `period` seconds on the given bus. - :param bus: The :class:`can.BusABC` to transmit to. - :param message: The :class:`can.Message` instance to periodically send + :param can.BusABC bus: A CAN bus which supports sending. + :param can.Message message: Message to send periodically. + :param float period: The minimum time between sending messages. :return: A started task instance """ - log.warning("The function `can.send_periodic` is deprecated and will " + - "be removed in version 2.3. Please use `can.Bus.send_periodic` instead.") + warnings.warn("The function `can.send_periodic` is deprecated and will " + + "be removed in an upcoming version. Please use `can.Bus.send_periodic` instead.", DeprecationWarning) return bus.send_periodic(message, period, *args, **kwargs) diff --git a/can/bus.py b/can/bus.py index c5ae7fdf0..eb666a6ab 100644 --- a/can/bus.py +++ b/can/bus.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -15,6 +14,7 @@ from .broadcastmanager import ThreadBasedCyclicSendTask +LOG = logging.getLogger(__name__) BusState = namedtuple('BusState', 'ACTIVE, PASSIVE, ERROR') @@ -22,11 +22,16 @@ class BusABC(object): """The CAN Bus Abstract Base Class that serves as the basis for all concrete interfaces. + + This class may be used as an iterator over the received messages. """ #: a string describing the underlying bus and/or channel channel_info = 'unknown' + #: Log level for received messages + RECV_LOGGING_LEVEL = 9 + @abstractmethod def __init__(self, channel, can_filters=None, **config): """Construct and open a CAN bus instance of the specified type. @@ -43,6 +48,7 @@ def __init__(self, channel, can_filters=None, **config): :param dict config: Any backend dependent configurations are passed in this dictionary """ + self._periodic_tasks = [] self.set_filters(can_filters) def __str__(self): @@ -51,7 +57,8 @@ def __str__(self): def recv(self, timeout=None): """Block waiting for a message from the Bus. - :param float timeout: + :type timeout: float or None + :param timeout: seconds to wait for a message or None to wait indefinitely :rtype: can.Message or None @@ -70,13 +77,15 @@ def recv(self, timeout=None): # return it, if it matches if msg and (already_filtered or self._matches_filters(msg)): + LOG.log(self.RECV_LOGGING_LEVEL, 'Received: %s', msg) return msg # if not, and timeout is None, try indefinitely elif timeout is None: continue - # try next one only if there still is time, and with reduced timeout + # try next one only if there still is time, and with + # reduced timeout else: time_left = timeout - (time() - start) @@ -107,12 +116,12 @@ def _recv_internal(self, timeout): .. note:: - The second return value (whether filtering was already done) may change - over time for some interfaces, like for example in the Kvaser interface. - Thus it cannot be simplified to a constant value. + The second return value (whether filtering was already done) may + change over time for some interfaces, like for example in the + Kvaser interface. Thus it cannot be simplified to a constant value. :param float timeout: seconds to wait for a message, - see :meth:`can.BusABC.send` + see :meth:`~can.BusABC.send` :rtype: tuple[can.Message, bool] or tuple[None, bool] :return: @@ -136,20 +145,31 @@ def send(self, msg, timeout=None): Override this method to enable the transmit path. :param can.Message msg: A message object. - :param float timeout: - If > 0, wait up to this many seconds for message to be ACK:ed or + + :type timeout: float or None + :param timeout: + If > 0, wait up to this many seconds for message to be ACK'ed or for transmit queue to be ready depending on driver implementation. If timeout is exceeded, an exception will be raised. Might not be supported by all interfaces. + None blocks indefinitly. :raises can.CanError: - if the message could not be written. + if the message could not be sent """ raise NotImplementedError("Trying to write to a readonly bus?") - def send_periodic(self, msg, period, duration=None): + def send_periodic(self, msg, period, duration=None, store_task=True): """Start sending a message at a given period on this bus. + The task will be active until one of the following conditions are met: + + - the (optional) duration expires + - the Bus instance goes out of scope + - the Bus instance is shutdown + - :meth:`Bus.stop_all_periodic_tasks()` is called + - the task's :meth:`Task.stop()` method is called. + :param can.Message msg: Message to transmit :param float period: @@ -157,8 +177,12 @@ def send_periodic(self, msg, period, duration=None): :param float duration: The duration to keep sending this message at given rate. If no duration is provided, the task will continue indefinitely. - - :return: A started task instance + :param bool store_task: + If True (the default) the task will be attached to this Bus instance. + Disable to instead manage tasks manually. + :return: + A started task instance. Note the task can be stopped (and depending on + the backend modified) by calling the :meth:`stop` method. :rtype: can.broadcastmanager.CyclicSendTaskABC .. note:: @@ -166,13 +190,63 @@ def send_periodic(self, msg, period, duration=None): Note the duration before the message stops being sent may not be exactly the same as the duration specified by the user. In general the message will be sent at the given rate until at - least *duration* seconds. + least **duration** seconds. + + .. note:: + + For extremely long running Bus instances with many short lived tasks the default + api with ``store_task==True`` may not be appropriate as the stopped tasks are + still taking up memory as they are associated with the Bus instance. + """ + task = self._send_periodic_internal(msg, period, duration) + # we wrap the task's stop method to also remove it from the Bus's list of tasks + original_stop_method = task.stop + + def wrapped_stop_method(remove_task=True): + if remove_task: + try: + self._periodic_tasks.remove(task) + except ValueError: + pass + original_stop_method() + task.stop = wrapped_stop_method + + if store_task: + self._periodic_tasks.append(task) + + return task + def _send_periodic_internal(self, msg, period, duration=None): + """Default implementation of periodic message sending using threading. + + Override this method to enable a more efficient backend specific approach. + + :param can.Message msg: + Message to transmit + :param float period: + Period in seconds between each message + :param float duration: + The duration to keep sending this message at given rate. If + no duration is provided, the task will continue indefinitely. + :return: + A started task instance. Note the task can be stopped (and depending on + the backend modified) by calling the :meth:`stop` method. + :rtype: can.broadcastmanager.CyclicSendTaskABC """ if not hasattr(self, "_lock_send_periodic"): # Create a send lock for this bus self._lock_send_periodic = threading.Lock() - return ThreadBasedCyclicSendTask(self, self._lock_send_periodic, msg, period, duration) + task = ThreadBasedCyclicSendTask(self, self._lock_send_periodic, msg, period, duration) + return task + + def stop_all_periodic_tasks(self, remove_tasks=True): + """Stop sending any messages that were started using bus.send_periodic + + :param bool remove_tasks: + Stop tracking the stopped tasks. + """ + for task in self._periodic_tasks: + task.stop(remove_task=remove_tasks) def __iter__(self): """Allow iteration on messages as they are received. @@ -205,22 +279,23 @@ def set_filters(self, filters=None): """Apply filtering to all messages received by this Bus. All messages that match at least one filter are returned. - If `filters` is `None` or a zero length sequence, all + If `filters` is `None` or a zero length sequence, all messages are matched. Calling without passing any filters will reset the applied filters to `None`. :param filters: - A iterable of dictionaries each containing a "can_id", a "can_mask", - and an optional "extended" key. + A iterable of dictionaries each containing a "can_id", + a "can_mask", and an optional "extended" key. >>> [{"can_id": 0x11, "can_mask": 0x21, "extended": False}] - A filter matches, when `` & can_mask == can_id & can_mask``. + A filter matches, when + `` & can_mask == can_id & can_mask``. If ``extended`` is set as well, it only matches messages where - `` == extended``. Else it matches every messages based - only on the arbitration ID and mask. + `` == extended``. Else it matches every + messages based only on the arbitration ID and mask. """ self._filters = filters or None self._apply_filters(self._filters) @@ -255,14 +330,15 @@ def _matches_filters(self, msg): for _filter in self._filters: # check if this filter even applies to the message if 'extended' in _filter and \ - _filter['extended'] != msg.is_extended_id: + _filter['extended'] != msg.is_extended_id: continue # then check for the mask and id can_id = _filter['can_id'] can_mask = _filter['can_mask'] - # basically, we compute `msg.arbitration_id & can_mask == can_id & can_mask` + # basically, we compute + # `msg.arbitration_id & can_mask == can_id & can_mask` # by using the shorter, but equivalent from below: if (can_id ^ msg.arbitration_id) & can_mask == 0: return True diff --git a/can/ctypesutil.py b/can/ctypesutil.py index 6dc372268..8a69b8df9 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -1,100 +1,99 @@ -#!/usr/bin/env python -# coding: utf-8 - -""" -This module contains common `ctypes` utils. -""" - -import binascii -import ctypes -import logging -import sys - -log = logging.getLogger('can.ctypesutil') - -__all__ = ['CLibrary', 'HANDLE', 'PHANDLE', 'HRESULT'] - -try: - _LibBase = ctypes.WinDLL -except AttributeError: - _LibBase = ctypes.CDLL - - -class LibraryMixin: - def map_symbol(self, func_name, restype=None, argtypes=(), errcheck=None): - """ - Map and return a symbol (function) from a C library. A reference to the - mapped symbol is also held in the instance - - :param str func_name: - symbol_name - :param ctypes.c_* restype: - function result type (i.e. ctypes.c_ulong...), defaults to void - :param tuple(ctypes.c_* ... ) argtypes: - argument types, defaults to no args - :param callable errcheck: - optional error checking function, see ctypes docs for _FuncPtr - """ - if (argtypes): - prototype = self.function_type(restype, *argtypes) - else: - prototype = self.function_type(restype) - try: - symbol = prototype((func_name, self)) - except AttributeError: - raise ImportError("Could not map function '{}' from library {}".format(func_name, self._name)) - - setattr(symbol, "_name", func_name) - log.debug('Wrapped function "{}", result type: {}, error_check {}'.format(func_name, type(restype), errcheck)) - - if (errcheck): - symbol.errcheck = errcheck - - setattr(self, func_name, symbol) - return symbol - - -class CLibrary_Win32(_LibBase, LibraryMixin): - " Basic ctypes.WinDLL derived class + LibraryMixin " - - def __init__(self, library_or_path): - if (isinstance(library_or_path, str)): - super(CLibrary_Win32, self).__init__(library_or_path) - else: - super(CLibrary_Win32, self).__init__(library_or_path._name, library_or_path._handle) - - @property - def function_type(self): - return ctypes.WINFUNCTYPE - - -class CLibrary_Unix(ctypes.CDLL, LibraryMixin): - " Basic ctypes.CDLL derived class + LibraryMixin " - - def __init__(self, library_or_path): - if (isinstance(library_or_path, str)): - super(CLibrary_Unix, self).__init__(library_or_path) - else: - super(CLibrary_Unix, self).__init__(library_or_path._name, library_or_path._handle) - - @property - def function_type(self): - return ctypes.CFUNCTYPE - - -if sys.platform == "win32": - CLibrary = CLibrary_Win32 - HRESULT = ctypes.HRESULT -else: - CLibrary = CLibrary_Unix - if sys.platform == "cygwin": - # Define HRESULT for cygwin - class HRESULT(ctypes.c_long): - pass - - -# Common win32 definitions -class HANDLE(ctypes.c_void_p): - pass - -PHANDLE = ctypes.POINTER(HANDLE) +# coding: utf-8 + +""" +This module contains common `ctypes` utils. +""" + +import binascii +import ctypes +import logging +import sys + +log = logging.getLogger('can.ctypesutil') + +__all__ = ['CLibrary', 'HANDLE', 'PHANDLE', 'HRESULT'] + +try: + _LibBase = ctypes.WinDLL +except AttributeError: + _LibBase = ctypes.CDLL + + +class LibraryMixin: + def map_symbol(self, func_name, restype=None, argtypes=(), errcheck=None): + """ + Map and return a symbol (function) from a C library. A reference to the + mapped symbol is also held in the instance + + :param str func_name: + symbol_name + :param ctypes.c_* restype: + function result type (i.e. ctypes.c_ulong...), defaults to void + :param tuple(ctypes.c_* ... ) argtypes: + argument types, defaults to no args + :param callable errcheck: + optional error checking function, see ctypes docs for _FuncPtr + """ + if (argtypes): + prototype = self.function_type(restype, *argtypes) + else: + prototype = self.function_type(restype) + try: + symbol = prototype((func_name, self)) + except AttributeError: + raise ImportError("Could not map function '{}' from library {}".format(func_name, self._name)) + + setattr(symbol, "_name", func_name) + log.debug('Wrapped function "{}", result type: {}, error_check {}'.format(func_name, type(restype), errcheck)) + + if (errcheck): + symbol.errcheck = errcheck + + setattr(self, func_name, symbol) + return symbol + + +class CLibrary_Win32(_LibBase, LibraryMixin): + " Basic ctypes.WinDLL derived class + LibraryMixin " + + def __init__(self, library_or_path): + if (isinstance(library_or_path, str)): + super(CLibrary_Win32, self).__init__(library_or_path) + else: + super(CLibrary_Win32, self).__init__(library_or_path._name, library_or_path._handle) + + @property + def function_type(self): + return ctypes.WINFUNCTYPE + + +class CLibrary_Unix(ctypes.CDLL, LibraryMixin): + " Basic ctypes.CDLL derived class + LibraryMixin " + + def __init__(self, library_or_path): + if (isinstance(library_or_path, str)): + super(CLibrary_Unix, self).__init__(library_or_path) + else: + super(CLibrary_Unix, self).__init__(library_or_path._name, library_or_path._handle) + + @property + def function_type(self): + return ctypes.CFUNCTYPE + + +if sys.platform == "win32": + CLibrary = CLibrary_Win32 + HRESULT = ctypes.HRESULT +else: + CLibrary = CLibrary_Unix + if sys.platform == "cygwin": + # Define HRESULT for cygwin + class HRESULT(ctypes.c_long): + pass + + +# Common win32 definitions +class HANDLE(ctypes.c_void_p): + pass + +PHANDLE = ctypes.POINTER(HANDLE) diff --git a/can/interface.py b/can/interface.py index b0e670942..78a18f891 100644 --- a/can/interface.py +++ b/can/interface.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -22,7 +21,7 @@ if 'linux' in sys.platform: # Deprecated and undocumented access to SocketCAN cyclic tasks - # Will be removed in version 3.0 + # Will be removed in version 4.0 from can.interfaces.socketcan import CyclicSendTask, MultiRateCyclicSendTask # Required by "detect_available_configs" for argument interpretation @@ -102,7 +101,12 @@ def __new__(cls, channel=None, *args, **config): # figure out the rest of the configuration; this might raise an error if channel is not None: config['channel'] = channel - config = load_config(config=config) + if 'context' in config: + context = config['context'] + del config['context'] + else: + context = None + config = load_config(config=config, context=context) # resolve the bus class to use for that interface cls = _get_class_for_interface(config['interface']) @@ -117,7 +121,11 @@ def __new__(cls, channel=None, *args, **config): channel = config['channel'] del config['channel'] - return cls(channel, *args, **config) + if channel is None: + # Use the default channel for the backend + return cls(*args, **config) + else: + return cls(channel, *args, **config) def detect_available_configs(interfaces=None): @@ -137,7 +145,7 @@ def detect_available_configs(interfaces=None): - `None` to search in all known interfaces. :rtype: list[dict] :return: an iterable of dicts, each suitable for usage in - :class:`can.interface.Bus`\ 's constructor. + the constructor of :class:`can.interface.Bus`. """ # Figure out where to search diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 6373d671d..e02dd2fb4 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -1,12 +1,13 @@ -#!/usr/bin/env python # coding: utf-8 """ Interfaces contain low level implementations that interact with CAN hardware. """ +import warnings from pkg_resources import iter_entry_points + # interface_name => (module, classname) BACKENDS = { 'kvaser': ('can.interfaces.kvaser', 'KvaserBus'), @@ -28,4 +29,10 @@ for interface in iter_entry_points('can.interface') }) +# Old entry point name. May be removed >3.0. +for interface in iter_entry_points('python_can.interface'): + BACKENDS[interface.name] = (interface.module_name, interface.attrs[0]) + warnings.warn('{} is using the deprecated python_can.interface entry point. '.format(interface.name) + + 'Please change to can.interface instead.', DeprecationWarning) + VALID_INTERFACES = frozenset(list(BACKENDS.keys()) + ['socketcan_native', 'socketcan_ctypes']) diff --git a/can/interfaces/ics_neovi/__init__.py b/can/interfaces/ics_neovi/__init__.py index 9e9f2b0ba..4426b1585 100644 --- a/can/interfaces/ics_neovi/__init__.py +++ b/can/interfaces/ics_neovi/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 6b164a0b0..6b0c95e69 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -1,10 +1,9 @@ -#!/usr/bin/env python # coding: utf-8 """ ICS NeoVi interface module. -python-ics is a Python wrapper around the API provided by Intrepid Control +python-ics is a Python wrapper around the API provided by Intrepid Control Systems for communicating with their NeoVI range of devices. Implementation references: @@ -69,13 +68,14 @@ class NeoViBus(BusABC): def __init__(self, channel, can_filters=None, **config): """ - - :param int channel: - The Channel id to create this bus with. + :param channel: + The channel ids to create this bus with. + Can also be a single integer, netid name or a comma separated + string. + :type channel: int or str or list(int) or list(str) :param list can_filters: See :meth:`can.BusABC.set_filters` for details. - - :param use_system_timestamp: + :param bool use_system_timestamp: Use system timestamp for can messages instead of the hardware time stamp :param str serial: @@ -84,6 +84,11 @@ def __init__(self, channel, can_filters=None, **config): :param int bitrate: Channel bitrate in bit/s. (optional, will enable the auto bitrate feature if not supplied) + :param bool fd: + If CAN-FD frames should be supported. + :param int data_bitrate: + Which bitrate to use for data phase in CAN FD. + Defaults to arbitration bitrate. """ if ics is None: raise ImportError('Please install python-ics') @@ -94,13 +99,14 @@ def __init__(self, channel, can_filters=None, **config): logger.info("CAN Filters: {}".format(can_filters)) logger.info("Got configuration of: {}".format(config)) - self._use_system_timestamp = bool( - config.get('use_system_timestamp', False) - ) - try: - channel = int(channel) - except ValueError: - raise ValueError('channel must be an integer') + if isinstance(channel, (list, tuple)): + self.channels = channel + elif isinstance(channel, int): + self.channels = [channel] + else: + # Assume comma separated string of channels + self.channels = [ch.strip() for ch in channel.split(',')] + self.channels = [NeoViBus.channel_to_netid(ch) for ch in self.channels] type_filter = config.get('type_filter') serial = config.get('serial') @@ -110,15 +116,39 @@ def __init__(self, channel, can_filters=None, **config): if 'bitrate' in config: ics.set_bit_rate(self.dev, config.get('bitrate'), channel) + fd = config.get('fd', False) + if fd: + if 'data_bitrate' in config: + ics.set_fd_bit_rate( + self.dev, config.get('data_bitrate'), channel) + + self._use_system_timestamp = bool( + config.get('use_system_timestamp', False) + ) + self.channel_info = '%s %s CH:%s' % ( self.dev.Name, self.get_serial_number(self.dev), - channel + self.channels ) logger.info("Using device: {}".format(self.channel_info)) self.rx_buffer = deque() - self.network = channel if channel is not None else None + + @staticmethod + def channel_to_netid(channel_name_or_id): + try: + channel = int(channel_name_or_id) + except ValueError: + netid = "NETID_{}".format(channel_name_or_id.upper()) + if hasattr(ics, netid): + channel = getattr(ics, netid) + else: + raise ValueError( + 'channel must be an integer or ' + 'a valid ICS channel name' + ) + return channel @staticmethod def get_serial_number(device): @@ -136,7 +166,7 @@ def get_serial_number(device): def shutdown(self): super(NeoViBus, self).shutdown() ics.close_device(self.dev) - + @staticmethod def _detect_available_configs(): """Detect all configurations/channels that this interface could @@ -148,10 +178,18 @@ def _detect_available_configs(): """ if ics is None: return [] + + try: + devices = ics.find_devices() + except Exception as e: + logger.debug("Failed to detect configs: %s", e) + return [] + # TODO: add the channel(s) return [{ + 'interface': 'neovi', 'serial': NeoViBus.get_serial_number(device) - } for device in ics.find_devices()] + } for device in devices] def _find_device(self, type_filter=None, serial=None): if type_filter is not None: @@ -180,7 +218,7 @@ def _process_msg_queue(self, timeout=0.1): except ics.RuntimeError: return for ics_msg in messages: - if ics_msg.NetworkID != self.network: + if ics_msg.NetworkID not in self.channels: continue self.rx_buffer.append(ics_msg) if errors: @@ -209,19 +247,49 @@ def _get_timestamp_for_msg(self, ics_msg): return ics.get_timestamp_for_msg(self.dev, ics_msg) def _ics_msg_to_message(self, ics_msg): - return Message( - timestamp=self._get_timestamp_for_msg(ics_msg), - arbitration_id=ics_msg.ArbIDOrHeader, - data=ics_msg.Data[:ics_msg.NumberBytesData], - dlc=ics_msg.NumberBytesData, - extended_id=bool( - ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME - ), - is_remote_frame=bool( - ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME - ), - channel=ics_msg.NetworkID - ) + is_fd = ics_msg.Protocol == ics.SPY_PROTOCOL_CANFD + + if is_fd: + if ics_msg.ExtraDataPtrEnabled: + data = ics_msg.ExtraDataPtr[:ics_msg.NumberBytesData] + else: + data = ics_msg.Data[:ics_msg.NumberBytesData] + + return Message( + timestamp=self._get_timestamp_for_msg(ics_msg), + arbitration_id=ics_msg.ArbIDOrHeader, + data=data, + dlc=ics_msg.NumberBytesData, + extended_id=bool( + ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME + ), + is_fd=is_fd, + is_remote_frame=bool( + ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME + ), + error_state_indicator=bool( + ics_msg.StatusBitField3 & ics.SPY_STATUS3_CANFD_ESI + ), + bitrate_switch=bool( + ics_msg.StatusBitField3 & ics.SPY_STATUS3_CANFD_BRS + ), + channel=ics_msg.NetworkID + ) + else: + return Message( + timestamp=self._get_timestamp_for_msg(ics_msg), + arbitration_id=ics_msg.ArbIDOrHeader, + data=ics_msg.Data[:ics_msg.NumberBytesData], + dlc=ics_msg.NumberBytesData, + extended_id=bool( + ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME + ), + is_fd=is_fd, + is_remote_frame=bool( + ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME + ), + channel=ics_msg.NetworkID + ) def _recv_internal(self, timeout=0.1): if not self.rx_buffer: @@ -234,22 +302,40 @@ def _recv_internal(self, timeout=0.1): return msg, False def send(self, msg, timeout=None): - if not self.dev.IsOpen: + if not ics.validate_hobject(self.dev): raise CanError("bus not open") + message = ics.SpyMessage() - flags = 0 + flag0 = 0 if msg.is_extended_id: - flags |= ics.SPY_STATUS_XTD_FRAME + flag0 |= ics.SPY_STATUS_XTD_FRAME if msg.is_remote_frame: - flags |= ics.SPY_STATUS_REMOTE_FRAME + flag0 |= ics.SPY_STATUS_REMOTE_FRAME + + flag3 = 0 + if msg.is_fd: + message.Protocol = ics.SPY_PROTOCOL_CANFD + if msg.bitrate_switch: + flag3 |= ics.SPY_STATUS3_CANFD_BRS + if msg.error_state_indicator: + flag3 |= ics.SPY_STATUS3_CANFD_ESI - message = ics.SpyMessage() message.ArbIDOrHeader = msg.arbitration_id message.NumberBytesData = len(msg.data) - message.Data = tuple(msg.data) - message.StatusBitField = flags + message.Data = tuple(msg.data[:8]) + if msg.is_fd and len(msg.data) > 8: + message.ExtraDataPtrEnabled = 1 + message.ExtraDataPtr = tuple(msg.data) + message.StatusBitField = flag0 message.StatusBitField2 = 0 - message.NetworkID = self.network + message.StatusBitField3 = flag3 + if msg.channel is not None: + message.NetworkID = msg.channel + elif len(self.channels) == 1: + message.NetworkID = self.channels[0] + else: + raise ValueError( + "msg.channel must be set when using multiple channels.") try: ics.transmit_messages(self.dev, message) diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index 7f127b241..da8dbffa4 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -36,7 +35,7 @@ def check_status(result, function, arguments): try: iscan = ctypes.cdll.LoadLibrary("iscandrv") -except Exception as e: +except OSError as e: iscan = None logger.warning("Failed to load IS-CAN driver: %s", e) else: diff --git a/can/interfaces/ixxat/__init__.py b/can/interfaces/ixxat/__init__.py index ab4e1f08c..aef26b729 100644 --- a/can/interfaces/ixxat/__init__.py +++ b/can/interfaces/ixxat/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index a34a23407..51812b147 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -279,6 +278,9 @@ def __init__(self, channel, can_filters=None, **config): :param list can_filters: See :meth:`can.BusABC.set_filters`. + :param bool receive_own_messages: + Enable self-reception of sent messages. + :param int UniqueHardwareId: UniqueHardwareId to connect (optional, will use the first found if not supplied) @@ -294,6 +296,7 @@ def __init__(self, channel, can_filters=None, **config): UniqueHardwareId = config.get('UniqueHardwareId', None) rxFifoSize = config.get('rxFifoSize', 16) txFifoSize = config.get('txFifoSize', 16) + self._receive_own_messages = config.get('receive_own_messages', False) # Usually comes as a string from the config file channel = int(channel) @@ -474,17 +477,16 @@ def _recv_internal(self, timeout): channel=self.channel ) - log.debug('Recv()ed message %s', rx_msg) return rx_msg, True def send(self, msg, timeout=None): - log.debug("Sending message: %s", msg) # This system is not designed to be very efficient message = structures.CANMSG() message.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA message.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0 - message.uMsgInfo.Bits.ext = 1 if msg.id_type else 0 + message.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0 + message.uMsgInfo.Bits.srr = 1 if self._receive_own_messages else 0 message.dwMsgId = msg.arbitration_id if msg.dlc: message.uMsgInfo.Bits.dlc = msg.dlc @@ -497,7 +499,7 @@ def send(self, msg, timeout=None): else: _canlib.canChannelPostMessage(self._channel_handle, message) - def send_periodic(self, msg, period, duration=None): + def _send_periodic_internal(self, msg, period, duration=None): """Send a message using built-in cyclic transmit list functionality.""" if self._scheduler is None: self._scheduler = HANDLE() @@ -543,7 +545,7 @@ def __init__(self, scheduler, msg, period, duration, resolution): self._msg.wCycleTime = int(round(period * resolution)) self._msg.dwMsgId = msg.arbitration_id self._msg.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA - self._msg.uMsgInfo.Bits.ext = 1 if msg.id_type else 0 + self._msg.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0 self._msg.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0 self._msg.uMsgInfo.Bits.dlc = msg.dlc for i, b in enumerate(msg.data): diff --git a/can/interfaces/ixxat/constants.py b/can/interfaces/ixxat/constants.py index 62505dcc5..d466e096d 100644 --- a/can/interfaces/ixxat/constants.py +++ b/can/interfaces/ixxat/constants.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/ixxat/exceptions.py b/can/interfaces/ixxat/exceptions.py index 9ac5b8f80..ac1700dca 100644 --- a/can/interfaces/ixxat/exceptions.py +++ b/can/interfaces/ixxat/exceptions.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/ixxat/structures.py b/can/interfaces/ixxat/structures.py index 72cab99b7..65b177d94 100644 --- a/can/interfaces/ixxat/structures.py +++ b/can/interfaces/ixxat/structures.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/kvaser/__init__.py b/can/interfaces/kvaser/__init__.py index c55ce39ed..5cbe63386 100644 --- a/can/interfaces/kvaser/__init__.py +++ b/can/interfaces/kvaser/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 0e0b4b93c..2d8305239 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -484,7 +483,7 @@ def _recv_internal(self, timeout=None): else: timeout = int(timeout * 1000) - log.log(9, 'Reading for %d ms on handle: %s' % (timeout, self._read_handle)) + #log.log(9, 'Reading for %d ms on handle: %s' % (timeout, self._read_handle)) status = canReadWait( self._read_handle, ctypes.byref(arb_id), @@ -517,8 +516,6 @@ def _recv_internal(self, timeout=None): error_state_indicator=error_state_indicator, channel=self.channel, timestamp=msg_timestamp + self._timestamp_offset) - rx_msg.flags = flags - rx_msg.raw_timestamp = msg_timestamp #log.debug('Got message: %s' % rx_msg) return rx_msg, self._is_filtered else: @@ -527,7 +524,7 @@ def _recv_internal(self, timeout=None): def send(self, msg, timeout=None): #log.debug("Writing a message: {}".format(msg)) - flags = canstat.canMSG_EXT if msg.id_type else canstat.canMSG_STD + flags = canstat.canMSG_EXT if msg.is_extended_id else canstat.canMSG_STD if msg.is_remote_frame: flags |= canstat.canMSG_RTR if msg.is_error_frame: diff --git a/can/interfaces/kvaser/constants.py b/can/interfaces/kvaser/constants.py index 1c658dce0..0188235be 100644 --- a/can/interfaces/kvaser/constants.py +++ b/can/interfaces/kvaser/constants.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index a5d92373c..38cca7504 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -264,7 +263,7 @@ def send(self, msg, timeout=None): It does not wait for message to be ACKed currently. """ arb_id = msg.arbitration_id - if msg.id_type: + if msg.is_extended_id: arb_id |= NC_FL_CAN_ARBID_XTD raw_msg = TxMessageStruct(arb_id, bool(msg.is_remote_frame), @@ -282,9 +281,11 @@ def send(self, msg, timeout=None): #nican.ncWaitForState( # self.handle, NC_ST_WRITE_SUCCESS, int(timeout * 1000), ctypes.byref(state)) - def flush_tx_buffer(self): + def reset(self): """ - Resets the CAN chip which includes clearing receive and transmit queues. + Resets network interface. Stops network interface, then resets the CAN + chip to clear the CAN error counters (clear error passive state). + Resetting includes clearing all entries from read and write queues. """ nican.ncAction(self.handle, NC_OP_RESET, 0) diff --git a/can/interfaces/pcan/__init__.py b/can/interfaces/pcan/__init__.py index 8dbcfd0f9..ceba250b5 100644 --- a/can/interfaces/pcan/__init__.py +++ b/can/interfaces/pcan/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/pcan/PCANBasic.py b/can/interfaces/pcan/basic.py similarity index 96% rename from can/interfaces/pcan/PCANBasic.py rename to can/interfaces/pcan/basic.py index 5d79ccdfb..322c6bbd4 100644 --- a/can/interfaces/pcan/PCANBasic.py +++ b/can/interfaces/pcan/basic.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -108,7 +107,7 @@ PCAN_LANBUS15 = TPCANHandle(0x80F) # PCAN-LAN interface, channel 15 PCAN_LANBUS16 = TPCANHandle(0x810) # PCAN-LAN interface, channel 16 -# Represent the PCAN error and status codes +# Represent the PCAN error and status codes PCAN_ERROR_OK = TPCANStatus(0x00000) # No error PCAN_ERROR_XMTFULL = TPCANStatus(0x00001) # Transmit buffer in CAN controller is full PCAN_ERROR_OVERRUN = TPCANStatus(0x00002) # CAN controller was read too late @@ -239,7 +238,7 @@ # Baud rate codes = BTR0/BTR1 register values for the CAN controller. # You can define your own Baud rate with the BTROBTR1 register. -# Take a look at www.peak-system.com for our free software "BAUDTOOL" +# Take a look at www.peak-system.com for our free software "BAUDTOOL" # to calculate the BTROBTR1 register for every bit rate and sample point. PCAN_BAUD_1M = TPCANBaudrate(0x0014) # 1 MBit/s @@ -260,7 +259,7 @@ # Represents the configuration for a CAN bit rate # Note: # * Each parameter and its value must be separated with a '='. -# * Each pair of parameter/value must be separated using ','. +# * Each pair of parameter/value must be separated using ','. # # Example: # f_clock=80000000,nom_brp=10,nom_tseg1=5,nom_tseg2=2,nom_sjw=1,data_brp=4,data_tseg1=7,data_tseg2=2,data_sjw=1 @@ -292,7 +291,7 @@ class TPCANMsg (Structure): """ Represents a PCAN message """ - _fields_ = [ ("ID", c_uint), # 11/29-bit message identifier + _fields_ = [ ("ID", c_ulong), # 11/29-bit message identifier - was changed from u_uint to c_ulong, so it is compatible with the PCAN-USB Driver for macOS ("MSGTYPE", TPCANMessageType), # Type of the message ("LEN", c_ubyte), # Data Length Code of the message (0..8) ("DATA", c_ubyte * 8) ] # Data of the message (DATA[0]..DATA[7]) @@ -303,7 +302,7 @@ class TPCANTimestamp (Structure): Represents a timestamp of a received PCAN message Total Microseconds = micros + 1000 * millis + 0x100000000 * 1000 * millis_overflow """ - _fields_ = [ ("millis", c_uint), # Base-value: milliseconds: 0.. 2^32-1 + _fields_ = [ ("millis", c_ulong), # Base-value: milliseconds: 0.. 2^32-1 - was changed from u_uint to c_ulong, so it is compatible with the PCAN-USB Driver for macOS ("millis_overflow", c_ushort), # Roll-arounds of millis ("micros", c_ushort) ] # Microseconds: 0..999 @@ -312,7 +311,7 @@ class TPCANMsgFD (Structure): """ Represents a PCAN message """ - _fields_ = [ ("ID", c_uint), # 11/29-bit message identifier + _fields_ = [ ("ID", c_ulong), # 11/29-bit message identifier - was changed from u_uint to c_ulong, so it is compatible with the PCAN-USB Driver for macOS ("MSGTYPE", TPCANMessageType), # Type of the message ("DLC", c_ubyte), # Data Length Code of the message (0..15) ("DATA", c_ubyte * 64) ] # Data of the message (DATA[0]..DATA[63]) @@ -329,6 +328,8 @@ def __init__(self): # Loads the PCANBasic.dll if platform.system() == 'Windows': self.__m_dllBasic = windll.LoadLibrary("PCANBasic") + elif platform.system() == 'Darwin': + self.__m_dllBasic = cdll.LoadLibrary('libPCBUSB.dylib') else: self.__m_dllBasic = cdll.LoadLibrary("libpcanbasic.so") if self.__m_dllBasic == None: @@ -351,7 +352,7 @@ def Initialize( HwType : NON PLUG&PLAY: The type of hardware and operation mode IOPort : NON PLUG&PLAY: The I/O address for the parallel port Interrupt: NON PLUG&PLAY: Interrupt number of the parallel port - + Returns: A TPCANStatus error code """ @@ -368,7 +369,7 @@ def InitializeFD( BitrateFD): """ - Initializes a FD capable PCAN Channel + Initializes a FD capable PCAN Channel Parameters: Channel : The handle of a FD capable PCAN Channel @@ -401,13 +402,13 @@ def Uninitialize( """ Uninitializes one or all PCAN Channels initialized by CAN_Initialize - + Remarks: Giving the TPCANHandle value "PCAN_NONEBUS", uninitialize all initialized channels - + Parameters: Channel : A TPCANHandle representing a PCAN Channel - + Returns: A TPCANStatus error code """ @@ -424,13 +425,13 @@ def Reset( """ Resets the receive and transmit queues of the PCAN Channel - + Remarks: A reset of the CAN controller is not performed - + Parameters: Channel : A TPCANHandle representing a PCAN Channel - + Returns: A TPCANStatus error code """ @@ -447,10 +448,10 @@ def GetStatus( """ Gets the current status of a PCAN Channel - + Parameters: Channel : A TPCANHandle representing a PCAN Channel - + Returns: A TPCANStatus error code """ @@ -469,16 +470,16 @@ def Read( Reads a CAN message from the receive queue of a PCAN Channel Remarks: - The return value of this method is a 3-touple, where + The return value of this method is a 3-touple, where the first value is the result (TPCANStatus) of the method. The order of the values are: [0]: A TPCANStatus error code [1]: A TPCANMsg structure with the CAN message read [2]: A TPCANTimestamp structure with the time when a message was read - + Parameters: Channel : A TPCANHandle representing a PCAN Channel - + Returns: A touple with three values """ @@ -499,16 +500,16 @@ def ReadFD( Reads a CAN message from the receive queue of a FD capable PCAN Channel Remarks: - The return value of this method is a 3-touple, where + The return value of this method is a 3-touple, where the first value is the result (TPCANStatus) of the method. The order of the values are: [0]: A TPCANStatus error code [1]: A TPCANMsgFD structure with the CAN message read [2]: A TPCANTimestampFD that is the time when a message was read - + Parameters: Channel : The handle of a FD capable PCAN Channel - + Returns: A touple with three values """ @@ -527,12 +528,12 @@ def Write( MessageBuffer): """ - Transmits a CAN message - + Transmits a CAN message + Parameters: Channel : A TPCANHandle representing a PCAN Channel MessageBuffer: A TPCANMsg representing the CAN message to be sent - + Returns: A TPCANStatus error code """ @@ -549,12 +550,12 @@ def WriteFD( MessageBuffer): """ - Transmits a CAN message over a FD capable PCAN Channel - + Transmits a CAN message over a FD capable PCAN Channel + Parameters: Channel : The handle of a FD capable PCAN Channel MessageBuffer: A TPCANMsgFD buffer with the message to be sent - + Returns: A TPCANStatus error code """ @@ -578,14 +579,14 @@ def FilterMessages( Remarks: The message filter will be expanded with every call to this function. If it is desired to reset the filter, please use the 'SetValue' function. - + Parameters: Channel : A TPCANHandle representing a PCAN Channel FromID : A c_uint value with the lowest CAN ID to be received ToID : A c_uint value with the highest CAN ID to be received - Mode : A TPCANMode representing the message type (Standard, 11-bit + Mode : A TPCANMode representing the message type (Standard, 11-bit identifier, or Extended, 29-bit identifier) - + Returns: A TPCANStatus error code """ @@ -608,15 +609,15 @@ def GetValue( Parameters can be present or not according with the kind of Hardware (PCAN Channel) being used. If a parameter is not available, a PCAN_ERROR_ILLPARAMTYPE error will be returned. - - The return value of this method is a 2-touple, where + + The return value of this method is a 2-touple, where the first value is the result (TPCANStatus) of the method and - the second one, the asked value - + the second one, the asked value + Parameters: Channel : A TPCANHandle representing a PCAN Channel Parameter : The TPCANParameter parameter to get - + Returns: A touple with 2 values """ @@ -646,13 +647,13 @@ def SetValue( Parameters can be present or not according with the kind of Hardware (PCAN Channel) being used. If a parameter is not available, a PCAN_ERROR_ILLPARAMTYPE error will be returned. - + Parameters: Channel : A TPCANHandle representing a PCAN Channel Parameter : The TPCANParameter parameter to set Buffer : Buffer with the value to be set BufferLength : Size in bytes of the buffer - + Returns: A TPCANStatus error code """ @@ -681,16 +682,16 @@ def GetErrorText( The current languages available for translation are: Neutral (0x00), German (0x07), English (0x09), Spanish (0x0A), - Italian (0x10) and French (0x0C) + Italian (0x10) and French (0x0C) The return value of this method is a 2-touple, where the first value is the result (TPCANStatus) of the method and the second one, the error text - + Parameters: Error : A TPCANStatus error code Language : Indicates a 'Primary language ID' (Default is Neutral(0)) - + Returns: A touple with 2 values """ diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 55009a623..0c1881d29 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -1,11 +1,10 @@ -#!/usr/bin/env python # coding: utf-8 """ Enable basic CAN over a PCAN USB device. """ -from __future__ import absolute_import, print_function +from __future__ import absolute_import, print_function, division import logging import sys @@ -13,8 +12,8 @@ import can from can import CanError, Message, BusABC -from .PCANBasic import * from can.bus import BusState +from .basic import * boottimeEpoch = 0 try: @@ -68,17 +67,18 @@ class PcanBus(BusABC): - def __init__(self, channel, state=BusState.ACTIVE, *args, **kwargs): + def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000, *args, **kwargs): """A PCAN USB interface to CAN. On top of the usual :class:`~can.Bus` methods provided, - the PCAN interface includes the :meth:`~can.interface.pcan.PcanBus.flash()` - and :meth:`~can.interface.pcan.PcanBus.status()` methods. + the PCAN interface includes the :meth:`~can.interface.pcan.PcanBus.flash` + and :meth:`~can.interface.pcan.PcanBus.status` methods. :param str channel: The can interface name. An example would be 'PCAN_USBBUS1' + Default is 'PCAN_USBBUS1' - :param BusState state: + :param can.bus.BusState state: BusState of the channel. Default is ACTIVE @@ -87,12 +87,7 @@ def __init__(self, channel, state=BusState.ACTIVE, *args, **kwargs): Default is 500 kbit/s. """ - if not channel: - raise ArgumentError("Must specify a PCAN channel") - else: - self.channel_info = channel - - bitrate = kwargs.get('bitrate', 500000) + self.channel_info = channel pcan_bitrate = pcan_bitrate_objs.get(bitrate, PCAN_BAUD_500K) hwtype = PCAN_TYPE_ISA @@ -119,7 +114,7 @@ def __init__(self, channel, state=BusState.ACTIVE, *args, **kwargs): if result != PCAN_ERROR_OK: raise PcanError(self._get_formatted_error(result)) - super(PcanBus, self).__init__(channel=channel, *args, **kwargs) + super(PcanBus, self).__init__(channel=channel, state=state, bitrate=bitrate, *args, **kwargs) def _get_formatted_error(self, error): """ @@ -160,7 +155,8 @@ def status(self): """ Query the PCAN bus status. - :return: The status code. See values in pcan_constants.py + :rtype: int + :return: The status code. See values in **basic.PCAN_ERROR_** """ return self.m_objPCANBasic.GetStatus(self.m_PcanHandle) @@ -187,7 +183,7 @@ def _recv_internal(self, timeout): # Calculate max time end_time = timeout_clock() + timeout - log.debug("Trying to read a msg") + #log.debug("Trying to read a msg") result = None while result is None: @@ -212,20 +208,13 @@ def _recv_internal(self, timeout): theMsg = result[1] itsTimeStamp = result[2] - log.debug("Received a message") + #log.debug("Received a message") bIsRTR = (theMsg.MSGTYPE & PCAN_MESSAGE_RTR.value) == PCAN_MESSAGE_RTR.value bIsExt = (theMsg.MSGTYPE & PCAN_MESSAGE_EXTENDED.value) == PCAN_MESSAGE_EXTENDED.value - if bIsExt: - #rx_msg.id_type = ID_TYPE_EXTENDED - log.debug("CAN: Extended") - else: - #rx_msg.id_type = ID_TYPE_STANDARD - log.debug("CAN: Standard") - dlc = theMsg.LEN - timestamp = boottimeEpoch + ((itsTimeStamp.micros + (1000 * itsTimeStamp.millis)) / (1000.0 * 1000.0)) + timestamp = boottimeEpoch + ((itsTimeStamp.micros + 1000 * itsTimeStamp.millis + 0x100000000 * 1000 * itsTimeStamp.millis_overflow) / (1000.0 * 1000.0)) rx_msg = Message(timestamp=timestamp, arbitration_id=theMsg.ID, @@ -237,7 +226,7 @@ def _recv_internal(self, timeout): return rx_msg, False def send(self, msg, timeout=None): - if msg.id_type: + if msg.is_extended_id: msgType = PCAN_MESSAGE_EXTENDED else: msgType = PCAN_MESSAGE_STANDARD diff --git a/can/interfaces/serial/__init__.py b/can/interfaces/serial/__init__.py index 6746fda0b..dced63b0f 100644 --- a/can/interfaces/serial/__init__.py +++ b/can/interfaces/serial/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 0184d2abc..d90107414 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -34,7 +33,8 @@ class SerialBus(BusABC): """ - def __init__(self, channel, baudrate=115200, timeout=0.1, *args, **kwargs): + def __init__(self, channel, baudrate=115200, timeout=0.1, rtscts=False, + *args, **kwargs): """ :param str channel: The serial device to open. For example "/dev/ttyS1" or @@ -49,12 +49,16 @@ def __init__(self, channel, baudrate=115200, timeout=0.1, *args, **kwargs): :param float timeout: Timeout for the serial device in seconds (default 0.1). + :param bool rtscts: + turn hardware handshake (RTS/CTS) on and off + """ if not channel: raise ValueError("Must specify a serial port.") self.channel_info = "Serial interface: " + channel - self.ser = serial.Serial(channel, baudrate=baudrate, timeout=timeout) + self.ser = serial.serial_for_url( + channel, baudrate=baudrate, timeout=timeout, rtscts=rtscts) super(SerialBus, self).__init__(channel=channel, *args, **kwargs) @@ -107,8 +111,8 @@ def _recv_internal(self, timeout): Read a message from the serial device. :param timeout: - - .. warning:: + + .. warning:: This parameter will be ignored. The timeout value of the channel is used. :returns: @@ -150,3 +154,9 @@ def _recv_internal(self, timeout): else: return None, False + + def fileno(self): + if hasattr(self.ser, 'fileno'): + return self.ser.fileno() + # Return an invalid file descriptor on Windows + return -1 diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py old mode 100755 new mode 100644 index 61a02f8b8..f115c239a --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -12,16 +11,20 @@ from __future__ import absolute_import -import io import time import logging -import serial - from can import BusABC, Message logger = logging.getLogger(__name__) +try: + import serial +except ImportError: + logger.warning("You won't be able to use the slcan can backend without " + "the serial module installed!") + serial = None + class slcanBus(BusABC): """ @@ -42,11 +45,12 @@ class slcanBus(BusABC): 83300: 'S9' } - _SLEEP_AFTER_SERIAL_OPEN = 2 # in seconds + _SLEEP_AFTER_SERIAL_OPEN = 2 # in seconds - def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwargs): + def __init__(self, channel, ttyBaudrate=115200, bitrate=None, + rtscts=False, **kwargs): """ - :param string channel: + :param str channel: port of underlying serial or usb device (e.g. /dev/ttyUSB0, COM8, ...) Must not be empty. :param int ttyBaudrate: @@ -55,19 +59,18 @@ def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwarg Bitrate in bit/s :param float poll_interval: Poll interval in seconds when reading messages - :param float timeout: - timeout in seconds when reading message + :param bool rtscts: + turn hardware handshake (RTS/CTS) on and off """ - if not channel: # if None or empty + if not channel: # if None or empty raise TypeError("Must specify a serial port.") if '@' in channel: (channel, ttyBaudrate) = channel.split('@') - self.serialPortOrig = serial.Serial(channel, baudrate=ttyBaudrate, timeout=timeout) - self.serialPort = io.TextIOWrapper(io.BufferedRWPair(self.serialPortOrig, self.serialPortOrig, 1), - newline='\r', line_buffering=True) + self.serialPortOrig = serial.serial_for_url( + channel, baudrate=ttyBaudrate, rtscts=rtscts) time.sleep(self._SLEEP_AFTER_SERIAL_OPEN) @@ -80,14 +83,14 @@ def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwarg self.open() - super(slcanBus, self).__init__(channel, ttyBaudrate=115200, timeout=1, - bitrate=None, **kwargs) + super(slcanBus, self).__init__(channel, ttyBaudrate=115200, + bitrate=None, rtscts=False, **kwargs) def write(self, string): if not string.endswith('\r'): string += '\r' - self.serialPort.write(string.decode()) - self.serialPort.flush() + self.serialPortOrig.write(string.encode()) + self.serialPortOrig.flush() def open(self): self.write('O') @@ -96,17 +99,20 @@ def close(self): self.write('C') def _recv_internal(self, timeout): - if timeout is not None: + if timeout != self.serialPortOrig.timeout: self.serialPortOrig.timeout = timeout canId = None remote = False extended = False frame = [] - readStr = self.serialPort.readline() + + readStr = self.serialPortOrig.read_until(b'\r') + if not readStr: return None, False else: + readStr = readStr.decode() if readStr[0] == 'T': # extended frame canId = int(readStr[1:9], 16) @@ -141,7 +147,10 @@ def _recv_internal(self, timeout): else: return None, False - def send(self, msg, timeout=None): + def send(self, msg, timeout=0): + if timeout != self.serialPortOrig.write_timeout: + self.serialPortOrig.write_timeout = timeout + if msg.is_remote_frame: if msg.is_extended_id: sendStr = "R%08X0" % (msg.arbitration_id) @@ -159,3 +168,9 @@ def send(self, msg, timeout=None): def shutdown(self): self.close() + + def fileno(self): + if hasattr(self.serialPortOrig, 'fileno'): + return self.serialPortOrig.fileno() + # Return an invalid file descriptor on Windows + return -1 diff --git a/can/interfaces/socketcan/__init__.py b/can/interfaces/socketcan/__init__.py index 338946136..8a2105598 100644 --- a/can/interfaces/socketcan/__init__.py +++ b/can/interfaces/socketcan/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/socketcan/constants.py b/can/interfaces/socketcan/constants.py index dc1b85ec3..3228d4f4e 100644 --- a/can/interfaces/socketcan/constants.py +++ b/can/interfaces/socketcan/constants.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index cc52e87f8..24a174867 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 import logging @@ -214,6 +213,7 @@ def send_bcm(bcm_socket, data): else: raise e + def _add_flags_to_can_id(message): can_id = message.arbitration_id if message.is_extended_id: @@ -240,21 +240,21 @@ class CyclicSendTask(LimitedDurationCyclicSendTaskABC, """ - def __init__(self, channel, message, period, duration=None): + def __init__(self, bcm_socket, message, period, duration=None): """ - :param str channel: The name of the CAN channel to connect to. + :param bcm_socket: An open bcm socket on the desired CAN channel. :param can.Message message: The message to be sent periodically. :param float period: The rate in seconds at which to send the message. :param float duration: Approximate duration in seconds to send the message. """ super(CyclicSendTask, self).__init__(message, period, duration) - self.channel = channel + self.bcm_socket = bcm_socket self.duration = duration self._tx_setup(message) self.message = message def _tx_setup(self, message): - self.bcm_socket = create_bcm_socket(self.channel) + # Create a low level packed frame to pass to the kernel self.can_id_with_flags = _add_flags_to_can_id(message) self.flags = CAN_FD_FRAME if message.is_fd else 0 @@ -283,7 +283,6 @@ def stop(self): stopframe = build_bcm_tx_delete_header(self.can_id_with_flags, self.flags) send_bcm(self.bcm_socket, stopframe) - self.bcm_socket.close() def modify_data(self, message): """Update the contents of this periodically sent message. @@ -391,7 +390,7 @@ def capture_message(sock, get_channel=False): raise can.CanError("Error receiving: %s" % exc) can_id, can_dlc, flags, data = dissect_can_frame(cf) - log.debug('Received: can_id=%x, can_dlc=%x, data=%s', can_id, can_dlc, data) + #log.debug('Received: can_id=%x, can_dlc=%x, data=%s', can_id, can_dlc, data) # Fetching the timestamp binary_structure = "@LL" @@ -413,11 +412,11 @@ def capture_message(sock, get_channel=False): error_state_indicator = bool(flags & CANFD_ESI) if is_extended_frame_format: - log.debug("CAN: Extended") + #log.debug("CAN: Extended") # TODO does this depend on SFF or EFF? arbitration_id = can_id & 0x1FFFFFFF else: - log.debug("CAN: Standard") + #log.debug("CAN: Standard") arbitration_id = can_id & 0x000007FF msg = Message(timestamp=timestamp, @@ -432,7 +431,7 @@ def capture_message(sock, get_channel=False): dlc=can_dlc, data=data) - log_rx.debug('Received: %s', msg) + #log_rx.debug('Received: %s', msg) return msg @@ -460,8 +459,9 @@ def __init__(self, channel="", receive_own_messages=False, fd=False, **kwargs): self.socket = create_socket() self.channel = channel self.channel_info = "socketcan channel '%s'" % channel + self._bcm_sockets = {} - # set the receive_own_messages paramater + # set the receive_own_messages parameter try: self.socket.setsockopt(SOL_CAN_RAW, CAN_RAW_RECV_OWN_MSGS, @@ -475,13 +475,23 @@ def __init__(self, channel="", receive_own_messages=False, fd=False, **kwargs): CAN_RAW_FD_FRAMES, 1) - bind_socket(self.socket, channel) + # Enable error frames + self.socket.setsockopt(SOL_CAN_RAW, + CAN_RAW_ERR_FILTER, + 0x1FFFFFFF) + bind_socket(self.socket, channel) kwargs.update({'receive_own_messages': receive_own_messages, 'fd': fd}) super(SocketcanBus, self).__init__(channel=channel, **kwargs) def shutdown(self): - """Closes the socket.""" + """Stops all active periodic tasks and closes the socket.""" + self.stop_all_periodic_tasks() + for channel in self._bcm_sockets: + log.debug("Closing bcm socket for channel {}".format(channel)) + bcm_socket = self._bcm_sockets[channel] + bcm_socket.close() + log.debug("Closing raw can socket") self.socket.close() def _recv_internal(self, timeout): @@ -560,7 +570,7 @@ def _send_once(self, data, channel=None): raise can.CanError("Failed to transmit: %s" % exc) return sent - def send_periodic(self, msg, period, duration=None): + def _send_periodic_internal(self, msg, period, duration=None): """Start sending a message at a given period on this bus. The kernel's broadcast manager will be used. @@ -573,7 +583,9 @@ def send_periodic(self, msg, period, duration=None): The duration to keep sending this message at given rate. If no duration is provided, the task will continue indefinitely. - :return: A started task instance + :return: + A started task instance. This can be used to modify the data, + pause/resume the transmission and to stop the transmission. :rtype: can.interfaces.socketcan.CyclicSendTask .. note:: @@ -584,7 +596,14 @@ def send_periodic(self, msg, period, duration=None): least *duration* seconds. """ - return CyclicSendTask(msg.channel or self.channel, msg, period, duration) + bcm_socket = self._get_bcm_socket(msg.channel or self.channel) + task = CyclicSendTask(bcm_socket, msg, period, duration) + return task + + def _get_bcm_socket(self, channel): + if channel not in self._bcm_sockets: + self._bcm_sockets[channel] = create_bcm_socket(self.channel) + return self._bcm_sockets[channel] def _apply_filters(self, filters): try: @@ -599,6 +618,9 @@ def _apply_filters(self, filters): else: self._is_filtered = True + def fileno(self): + return self.socket.fileno() + @staticmethod def _detect_available_configs(): return [{'interface': 'socketcan', 'channel': channel} diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py index ef522e408..44d356920 100644 --- a/can/interfaces/socketcan/utils.py +++ b/can/interfaces/socketcan/utils.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/usb2can/__init__.py b/can/interfaces/usb2can/__init__.py index 8262dc47b..454942934 100644 --- a/can/interfaces/usb2can/__init__.py +++ b/can/interfaces/usb2can/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index 3e9490a01..422347af0 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -23,8 +22,8 @@ def WMIDateStringToDate(dtmDate): strDateTime = strDateTime + dtmDate[7] + '/' else: strDateTime = strDateTime + dtmDate[6] + dtmDate[7] + '/' - strDateTime = strDateTime + dtmDate[0] + dtmDate[1] + dtmDate[2] + dtmDate[3] + ' ' + dtmDate[8] + dtmDate[ - 9] + ':' + dtmDate[10] + dtmDate[11] + ':' + dtmDate[12] + dtmDate[13] + strDateTime = strDateTime + dtmDate[0] + dtmDate[1] + dtmDate[2] + dtmDate[3] + ' ' + dtmDate[8] + dtmDate[9] \ + + ':' + dtmDate[10] + dtmDate[11] + ':' + dtmDate[12] + dtmDate[13] return strDateTime @@ -38,7 +37,4 @@ def serial(): string = objItem.Dependent # find based on beginning of serial if 'ED' in string: - # print "Dependent:" + ` objItem.Dependent` - string = string[len(string) - 9:len(string) - 1] - - return string + return string[len(string) - 9:len(string) - 1] diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index 3a22ae5b4..fee9e14ab 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -52,7 +51,7 @@ def message_convert_tx(msg): if msg.is_remote_frame: messagetx.flags |= IS_REMOTE_FRAME - if msg.id_type: + if msg.is_extended_id: messagetx.flags |= IS_ID_TYPE return messagetx @@ -112,7 +111,7 @@ def __init__(self, channel, *args, **kwargs): deviceID = serial() # get baudrate in b/s from bitrate or use default - bitrate = kwargs.get("bitrate", d=500000) + bitrate = kwargs.get("bitrate", 500000) # convert to kb/s (eg:500000 bitrate must be 500), max rate is 1000 kb/s baudrate = min(1000, int(bitrate/1000)) diff --git a/can/interfaces/usb2can/usb2canabstractionlayer.py b/can/interfaces/usb2can/usb2canabstractionlayer.py index 608c1dca8..903b6d458 100644 --- a/can/interfaces/usb2can/usb2canabstractionlayer.py +++ b/can/interfaces/usb2can/usb2canabstractionlayer.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/vector/__init__.py b/can/interfaces/vector/__init__.py index 9342e6d60..dac47be4a 100644 --- a/can/interfaces/vector/__init__.py +++ b/can/interfaces/vector/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 33551deb9..7154d5d38 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -50,7 +49,7 @@ class VectorBus(BusABC): def __init__(self, channel, can_filters=None, poll_interval=0.01, receive_own_messages=False, - bitrate=None, rx_queue_size=2**14, app_name="CANalyzer", fd=False, data_bitrate=None, sjwAbr=2, tseg1Abr=6, tseg2Abr=3, sjwDbr=2, tseg1Dbr=6, tseg2Dbr=3, **config): + bitrate=None, rx_queue_size=2**14, app_name="CANalyzer", serial=None, fd=False, data_bitrate=None, sjwAbr=2, tseg1Abr=6, tseg2Abr=3, sjwDbr=2, tseg1Dbr=6, tseg2Dbr=3, **config): """ :param list channel: The channel indexes to create this bus with. @@ -65,6 +64,11 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, CAN-FD: range 8192…524288 :param str app_name: Name of application in Hardware Config. + If set to None, the channel should be a global channel index. + :param int serial: + Serial number of the hardware to be used. + If set, the channel parameter refers to the channels ONLY on the specified hardware. + If set, the app_name is unused. :param bool fd: If CAN-FD frames should be supported. :param int data_bitrate: @@ -85,6 +89,22 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, self.channel_info = 'Application %s: %s' % ( app_name, ', '.join('CAN %d' % (ch + 1) for ch in self.channels)) + if serial is not None: + app_name = None + channel_index = [] + channel_configs = get_channel_configs() + for channel_config in channel_configs: + if channel_config.serialNumber == serial: + if channel_config.hwChannel in self.channels: + channel_index.append(channel_config.channelIndex) + if len(channel_index) > 0: + if len(channel_index) != len(self.channels): + LOG.info("At least one defined channel wasn't found on the specified hardware.") + self.channels = channel_index + else: + # Is there any better way to raise the error? + raise Exception("None of the configured channels could be found on the specified hardware.") + vxlapi.xlOpenDriver() self.port_handle = vxlapi.XLportHandle(vxlapi.XL_INVALID_PORTHANDLE) self.mask = 0 @@ -92,26 +112,30 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, # Get channels masks self.channel_masks = {} self.index_to_channel = {} + for channel in self.channels: - hw_type = ctypes.c_uint(0) - hw_index = ctypes.c_uint(0) - hw_channel = ctypes.c_uint(0) - vxlapi.xlGetApplConfig(self._app_name, channel, hw_type, hw_index, - hw_channel, vxlapi.XL_BUS_TYPE_CAN) - LOG.debug('Channel index %d found', channel) - idx = vxlapi.xlGetChannelIndex(hw_type.value, hw_index.value, - hw_channel.value) - if idx < 0: - # Undocumented behavior! See issue #353. - # If hardware is unavailable, this function returns -1. - # Raise an exception as if the driver - # would have signalled XL_ERR_HW_NOT_PRESENT. - raise VectorError(vxlapi.XL_ERR_HW_NOT_PRESENT, - "XL_ERR_HW_NOT_PRESENT", - "xlGetChannelIndex") + if app_name: + # Get global channel index from application channel + hw_type = ctypes.c_uint(0) + hw_index = ctypes.c_uint(0) + hw_channel = ctypes.c_uint(0) + vxlapi.xlGetApplConfig(self._app_name, channel, hw_type, hw_index, + hw_channel, vxlapi.XL_BUS_TYPE_CAN) + LOG.debug('Channel index %d found', channel) + idx = vxlapi.xlGetChannelIndex(hw_type.value, hw_index.value, + hw_channel.value) + if idx < 0: + # Undocumented behavior! See issue #353. + # If hardware is unavailable, this function returns -1. + # Raise an exception as if the driver + # would have signalled XL_ERR_HW_NOT_PRESENT. + raise VectorError(vxlapi.XL_ERR_HW_NOT_PRESENT, + "XL_ERR_HW_NOT_PRESENT", + "xlGetChannelIndex") + else: + # Channel already given as global channel + idx = channel mask = 1 << idx - LOG.debug('Channel %d, Type: %d, Mask: 0x%X', - hw_channel.value, hw_type.value, mask) self.channel_masks[channel] = mask self.index_to_channel[idx] = channel self.mask |= mask @@ -295,7 +319,7 @@ def _recv_internal(self, timeout): def send(self, msg, timeout=None): msg_id = msg.arbitration_id - if msg.id_type: + if msg.is_extended_id: msg_id |= vxlapi.XL_CAN_EXT_MSG_ID flags = 0 @@ -355,4 +379,29 @@ def reset(self): vxlapi.xlDeactivateChannel(self.port_handle, self.mask) vxlapi.xlActivateChannel(self.port_handle, self.mask, vxlapi.XL_BUS_TYPE_CAN, 0) - + + @staticmethod + def _detect_available_configs(): + configs = [] + channel_configs = get_channel_configs() + LOG.info('Found %d channels', len(channel_configs)) + for channel_config in channel_configs: + LOG.info('Channel index %d: %s', + channel_config.channelIndex, + channel_config.name.decode('ascii')) + configs.append({'interface': 'vector', + 'app_name': None, + 'channel': channel_config.channelIndex}) + return configs + +def get_channel_configs(): + if vxlapi is None: + return [] + driver_config = vxlapi.XLdriverConfig() + try: + vxlapi.xlOpenDriver() + vxlapi.xlGetDriverConfig(driver_config) + vxlapi.xlCloseDriver() + except: + pass + return [driver_config.channel[i] for i in range(driver_config.channelCount)] diff --git a/can/interfaces/vector/exceptions.py b/can/interfaces/vector/exceptions.py index ab50ff60d..8715c276f 100644 --- a/can/interfaces/vector/exceptions.py +++ b/can/interfaces/vector/exceptions.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py index a5f26f80f..ae87706c4 100644 --- a/can/interfaces/vector/vxlapi.py +++ b/can/interfaces/vector/vxlapi.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -52,7 +51,7 @@ XL_CAN_STD = 1 XL_CAN_EXT = 2 -XLuint64 = ctypes.c_ulonglong +XLuint64 = ctypes.c_int64 XLaccess = XLuint64 XLhandle = ctypes.c_void_p @@ -145,6 +144,55 @@ class XLcanFdConf(ctypes.Structure): ('tseg1Dbr', ctypes.c_uint), ('tseg2Dbr', ctypes.c_uint), ('reserved', ctypes.c_uint * 2)] +class XLchannelConfig(ctypes.Structure): + _pack_ = 1 + _fields_ = [ + ('name', ctypes.c_char * 32), + ('hwType', ctypes.c_ubyte), + ('hwIndex', ctypes.c_ubyte), + ('hwChannel', ctypes.c_ubyte), + ('transceiverType', ctypes.c_ushort), + ('transceiverState', ctypes.c_ushort), + ('configError', ctypes.c_ushort), + ('channelIndex', ctypes.c_ubyte), + ('channelMask', XLuint64), + ('channelCapabilities', ctypes.c_uint), + ('channelBusCapabilities', ctypes.c_uint), + ('isOnBus', ctypes.c_ubyte), + ('connectedBusType', ctypes.c_uint), + ('busParams', ctypes.c_ubyte * 32), + ('_doNotUse', ctypes.c_uint), + ('driverVersion', ctypes.c_uint), + ('interfaceVersion', ctypes.c_uint), + ('raw_data', ctypes.c_uint * 10), + ('serialNumber', ctypes.c_uint), + ('articleNumber', ctypes.c_uint), + ('transceiverName', ctypes.c_char * 32), + ('specialCabFlags', ctypes.c_uint), + ('dominantTimeout', ctypes.c_uint), + ('dominantRecessiveDelay', ctypes.c_ubyte), + ('recessiveDominantDelay', ctypes.c_ubyte), + ('connectionInfo', ctypes.c_ubyte), + ('currentlyAvailableTimestamps', ctypes.c_ubyte), + ('minimalSupplyVoltage', ctypes.c_ushort), + ('maximalSupplyVoltage', ctypes.c_ushort), + ('maximalBaudrate', ctypes.c_uint), + ('fpgaCoreCapabilities', ctypes.c_ubyte), + ('specialDeviceStatus', ctypes.c_ubyte), + ('channelBusActiveCapabilities', ctypes.c_ushort), + ('breakOffset', ctypes.c_ushort), + ('delimiterOffset', ctypes.c_ushort), + ('reserved', ctypes.c_uint * 3) + ] + +class XLdriverConfig(ctypes.Structure): + _fields_ = [ + ('dllVersion', ctypes.c_uint), + ('channelCount', ctypes.c_uint), + ('reserved', ctypes.c_uint * 10), + ('channel', XLchannelConfig * 64) + ] + # driver status XLstatus = ctypes.c_short @@ -159,6 +207,11 @@ def check_status(result, function, arguments): return result +xlGetDriverConfig = _xlapi_dll.xlGetDriverConfig +xlGetDriverConfig.argtypes = [ctypes.POINTER(XLdriverConfig)] +xlGetDriverConfig.restype = XLstatus +xlGetDriverConfig.errcheck = check_status + xlOpenDriver = _xlapi_dll.xlOpenDriver xlOpenDriver.argtypes = [] xlOpenDriver.restype = XLstatus diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index c53d0cf21..ce863dc5e 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/io/__init__.py b/can/io/__init__.py index 1dc412d52..967b9e555 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -18,4 +17,4 @@ from .canutils import CanutilsLogReader, CanutilsLogWriter from .csv import CSVWriter, CSVReader from .sqlite import SqliteReader, SqliteWriter -from .stdout import Printer +from .printer import Printer diff --git a/can/io/asc.py b/can/io/asc.py index 7da32d067..3feb6755c 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -1,19 +1,23 @@ -#!/usr/bin/env python # coding: utf-8 """ Contains handling of ASC logging files. -Example .asc file: https://bitbucket.org/tobylorenz/vector_asc/src/47556e1a6d32c859224ca62d075e1efcc67fa690/src/Vector/ASC/tests/unittests/data/CAN_Log_Trigger_3_2.asc?at=master&fileviewer=file-view-default +Example .asc files: + - https://bitbucket.org/tobylorenz/vector_asc/src/47556e1a6d32c859224ca62d075e1efcc67fa690/src/Vector/ASC/tests/unittests/data/CAN_Log_Trigger_3_2.asc?at=master&fileviewer=file-view-default + - under `test/data/logfile.asc` """ +from __future__ import absolute_import + from datetime import datetime import time import logging -from can.listener import Listener -from can.message import Message -from can.util import channel2int +from ..message import Message +from ..listener import Listener +from ..util import channel2int +from .generic import BaseIOHandler CAN_MSG_EXT = 0x80000000 CAN_ID_MASK = 0x1FFFFFFF @@ -21,15 +25,20 @@ logger = logging.getLogger('can.io.asc') -class ASCReader(object): +class ASCReader(BaseIOHandler): """ Iterator of CAN messages from a ASC logging file. - TODO: turn realtive timestamps back to absolute form + TODO: turn relative timestamps back to absolute form """ - def __init__(self, filename): - self.file = open(filename, 'r') + def __init__(self, file): + """ + :param file: a path-like object or as file-like object to read from + If this is a file-like object, is has to opened in text + read mode, not binary read mode. + """ + super(ASCReader, self).__init__(file, mode='r') @staticmethod def _extract_can_id(str_can_id): @@ -39,19 +48,19 @@ def _extract_can_id(str_can_id): else: is_extended = False can_id = int(str_can_id, 16) - logging.debug('ASCReader: _extract_can_id("%s") -> %x, %r', str_can_id, can_id, is_extended) - return (can_id, is_extended) + #logging.debug('ASCReader: _extract_can_id("%s") -> %x, %r', str_can_id, can_id, is_extended) + return can_id, is_extended def __iter__(self): for line in self.file: - logger.debug("ASCReader: parsing line: '%s'", line.splitlines()[0]) + #logger.debug("ASCReader: parsing line: '%s'", line.splitlines()[0]) temp = line.strip() if not temp or not temp[0].isdigit(): continue try: - (timestamp, channel, dummy) = temp.split(None, 2) # , frameType, dlc, frameData + timestamp, channel, dummy = temp.split(None, 2) # , frameType, dlc, frameData except ValueError: # we parsed an empty comment continue @@ -72,8 +81,8 @@ def __iter__(self): pass elif dummy[-1:].lower() == 'r': - (can_id_str, _) = dummy.split(None, 1) - (can_id_num, is_extended_id) = self._extract_can_id(can_id_str) + can_id_str, _ = dummy.split(None, 1) + can_id_num, is_extended_id = self._extract_can_id(can_id_str) msg = Message(timestamp=timestamp, arbitration_id=can_id_num & CAN_ID_MASK, extended_id=is_extended_id, @@ -84,10 +93,10 @@ def __iter__(self): else: try: # this only works if dlc > 0 and thus data is availabe - (can_id_str, _, _, dlc, data) = dummy.split(None, 4) + can_id_str, _, _, dlc, data = dummy.split(None, 4) except ValueError: # but if not, we only want to get the stuff up to the dlc - (can_id_str, _, _, dlc ) = dummy.split(None, 3) + can_id_str, _, _, dlc = dummy.split(None, 3) # and we set data to an empty sequence manually data = '' @@ -97,24 +106,26 @@ def __iter__(self): for byte in data[0:dlc]: frame.append(int(byte, 16)) - (can_id_num, is_extended_id) = self._extract_can_id(can_id_str) + can_id_num, is_extended_id = self._extract_can_id(can_id_str) - msg = Message( - timestamp=timestamp, - arbitration_id=can_id_num & CAN_ID_MASK, - extended_id=is_extended_id, - is_remote_frame=False, - dlc=dlc, - data=frame, - channel=channel) - yield msg + yield Message( + timestamp=timestamp, + arbitration_id=can_id_num & CAN_ID_MASK, + extended_id=is_extended_id, + is_remote_frame=False, + dlc=dlc, + data=frame, + channel=channel + ) + + self.stop() -class ASCWriter(Listener): +class ASCWriter(BaseIOHandler, Listener): """Logs CAN data to an ASCII log file (.asc). The measurement starts with the timestamp of the first registered message. - If a message has a timestamp smaller than the previous one (or 0 or None), + If a message has a timestamp smaller than the previous one or None, it gets assigned the timestamp that was written for the last message. It the first message does not have a timestamp, it is set to zero. """ @@ -123,16 +134,22 @@ class ASCWriter(Listener): FORMAT_DATE = "%a %b %m %I:%M:%S %p %Y" FORMAT_EVENT = "{timestamp: 9.4f} {message}\n" - def __init__(self, filename, channel=1): - # setup + def __init__(self, file, channel=1): + """ + :param file: a path-like object or as file-like object to write to + If this is a file-like object, is has to opened in text + write mode, not binary write mode. + :param channel: a default channel to use when the message does not + have a channel set + """ + super(ASCWriter, self).__init__(file, mode='w') self.channel = channel - self.log_file = open(filename, 'w') # write start of file header now = datetime.now().strftime("%a %b %m %I:%M:%S %p %Y") - self.log_file.write("date %s\n" % now) - self.log_file.write("base hex timestamps absolute\n") - self.log_file.write("internal events logged\n") + self.file.write("date %s\n" % now) + self.file.write("base hex timestamps absolute\n") + self.file.write("internal events logged\n") # the last part is written with the timestamp of the first message self.header_written = False @@ -140,10 +157,9 @@ def __init__(self, filename, channel=1): self.started = None def stop(self): - """Stops logging and closes the file.""" - if not self.log_file.closed: - self.log_file.write("End TriggerBlock\n") - self.log_file.close() + if not self.file.closed: + self.file.write("End TriggerBlock\n") + super(ASCWriter, self).stop() def log_event(self, message, timestamp=None): """Add a message to the log file. @@ -161,10 +177,9 @@ def log_event(self, message, timestamp=None): self.last_timestamp = (timestamp or 0.0) self.started = self.last_timestamp formatted_date = time.strftime(self.FORMAT_DATE, time.localtime(self.last_timestamp)) - self.log_file.write("base hex timestamps absolute\n") - self.log_file.write("Begin Triggerblock %s\n" % formatted_date) + self.file.write("Begin Triggerblock %s\n" % formatted_date) self.header_written = True - self.log_event("Start of measurement") # recursive call + self.log_event("Start of measurement") # caution: this is a recursive call! # figure out the correct timestamp if timestamp is None or timestamp < self.last_timestamp: @@ -175,11 +190,7 @@ def log_event(self, message, timestamp=None): timestamp -= self.started line = self.FORMAT_EVENT.format(timestamp=timestamp, message=message) - - if self.log_file.closed: - logger.warn("ASCWriter: ignoring write call to closed file") - else: - self.log_file.write(line) + self.file.write(line) def on_message_received(self, msg): diff --git a/can/io/blf.py b/can/io/blf.py index 8115e04e2..df8f611b0 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -15,6 +14,8 @@ objects types. """ +from __future__ import absolute_import + import struct import zlib import datetime @@ -24,6 +25,7 @@ from can.message import Message from can.listener import Listener from can.util import len2dlc, dlc2len, channel2int +from .generic import BaseIOHandler class BLFParseError(Exception): @@ -112,7 +114,7 @@ def systemtime_to_timestamp(systemtime): return 0 -class BLFReader(object): +class BLFReader(BaseIOHandler): """ Iterator of CAN messages from a Binary Logging File. @@ -120,11 +122,15 @@ class BLFReader(object): silently ignored. """ - def __init__(self, filename): - self.fp = open(filename, "rb") - data = self.fp.read(FILE_HEADER_STRUCT.size) + def __init__(self, file): + """ + :param file: a path-like object or as file-like object to read from + If this is a file-like object, is has to opened in binary + read mode, not text read mode. + """ + super(BLFReader, self).__init__(file, mode='rb') + data = self.file.read(FILE_HEADER_STRUCT.size) header = FILE_HEADER_STRUCT.unpack(data) - #print(header) if header[0] != b"LOGG": raise BLFParseError("Unexpected file format") self.file_size = header[10] @@ -133,25 +139,24 @@ def __init__(self, filename): self.start_timestamp = systemtime_to_timestamp(header[14:22]) self.stop_timestamp = systemtime_to_timestamp(header[22:30]) # Read rest of header - self.fp.read(header[1] - FILE_HEADER_STRUCT.size) + self.file.read(header[1] - FILE_HEADER_STRUCT.size) def __iter__(self): tail = b"" while True: - data = self.fp.read(OBJ_HEADER_BASE_STRUCT.size) + data = self.file.read(OBJ_HEADER_BASE_STRUCT.size) if not data: # EOF break header = OBJ_HEADER_BASE_STRUCT.unpack(data) - #print(header) if header[0] != b"LOBJ": raise BLFParseError() obj_type = header[4] obj_data_size = header[3] - OBJ_HEADER_BASE_STRUCT.size - obj_data = self.fp.read(obj_data_size) + obj_data = self.file.read(obj_data_size) # Read padding bytes - self.fp.read(obj_data_size % 4) + self.file.read(obj_data_size % 4) if obj_type == LOG_CONTAINER: method, uncompressed_size = LOG_CONTAINER_STRUCT.unpack_from( @@ -245,13 +250,13 @@ def __iter__(self): pos = next_pos - # Save remaing data that could not be processed + # save the remaining data that could not be processed tail = data[pos:] - self.fp.close() + self.stop() -class BLFWriter(Listener): +class BLFWriter(BaseIOHandler, Listener): """ Logs CAN data to a Binary Logging File compatible with Vector's tools. """ @@ -262,11 +267,16 @@ class BLFWriter(Listener): #: ZLIB compression level COMPRESSION_LEVEL = 9 - def __init__(self, filename, channel=1): - self.fp = open(filename, "wb") + def __init__(self, file, channel=1): + """ + :param file: a path-like object or as file-like object to write to + If this is a file-like object, is has to opened in binary + write mode, not text write mode. + """ + super(BLFWriter, self).__init__(file, mode='wb') self.channel = channel # Header will be written after log is done - self.fp.write(b"\x00" * FILE_HEADER_SIZE) + self.file.write(b"\x00" * FILE_HEADER_SIZE) self.cache = [] self.cache_size = 0 self.count_of_objects = 0 @@ -283,7 +293,7 @@ def on_message_received(self, msg): channel += 1 arb_id = msg.arbitration_id - if msg.id_type: + if msg.is_extended_id: arb_id |= CAN_MSG_EXT flags = REMOTE_FLAG if msg.is_remote_frame else 0 data = bytes(msg.data) @@ -360,7 +370,7 @@ def _add_object(self, obj_type, data, timestamp=None): def _flush(self): """Compresses and writes data in the cache to file.""" - if self.fp.closed: + if self.file.closed: return cache = b"".join(self.cache) if not cache: @@ -379,21 +389,19 @@ def _flush(self): b"LOBJ", OBJ_HEADER_BASE_STRUCT.size, 1, obj_size, LOG_CONTAINER) container_header = LOG_CONTAINER_STRUCT.pack( ZLIB_DEFLATE, len(uncompressed_data)) - self.fp.write(base_header) - self.fp.write(container_header) - self.fp.write(compressed_data) + self.file.write(base_header) + self.file.write(container_header) + self.file.write(compressed_data) # Write padding bytes - self.fp.write(b"\x00" * (obj_size % 4)) + self.file.write(b"\x00" * (obj_size % 4)) self.uncompressed_size += OBJ_HEADER_V1_STRUCT.size + LOG_CONTAINER_STRUCT.size self.uncompressed_size += len(uncompressed_data) def stop(self): """Stops logging and closes the file.""" - if self.fp.closed: - return self._flush() - filesize = self.fp.tell() - self.fp.close() + filesize = self.file.tell() + super(BLFWriter, self).stop() # Write header in the beginning of the file header = [b"LOGG", FILE_HEADER_SIZE, @@ -403,5 +411,5 @@ def stop(self): self.count_of_objects, 0]) header.extend(timestamp_to_systemtime(self.start_timestamp)) header.extend(timestamp_to_systemtime(self.stop_timestamp)) - with open(self.fp.name, "r+b") as f: + with open(self.file.name, "r+b") as f: f.write(FILE_HEADER_STRUCT.pack(*header)) diff --git a/can/io/canutils.py b/can/io/canutils.py index 564b386e1..40b9ec2b6 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -7,12 +6,15 @@ (https://github.com/linux-can/can-utils). """ +from __future__ import absolute_import, division + import time import datetime import logging from can.message import Message from can.listener import Listener +from .generic import BaseIOHandler log = logging.getLogger('can.io.canutils') @@ -23,7 +25,7 @@ CAN_ERR_DLC = 8 -class CanutilsLogReader(object): +class CanutilsLogReader(BaseIOHandler): """ Iterator over CAN messages from a .log Logging File (candump -L). @@ -33,51 +35,60 @@ class CanutilsLogReader(object): ``(0.0) vcan0 001#8d00100100820100`` """ - def __init__(self, filename): - self.fp = open(filename, 'r') + def __init__(self, file): + """ + :param file: a path-like object or as file-like object to read from + If this is a file-like object, is has to opened in text + read mode, not binary read mode. + """ + super(CanutilsLogReader, self).__init__(file, mode='r') def __iter__(self): - for line in self.fp: + for line in self.file: + + # skip empty lines temp = line.strip() + if not temp: + continue - if temp: + timestamp, channel, frame = temp.split() + timestamp = float(timestamp[1:-1]) + canId, data = frame.split('#') + if channel.isdigit(): + channel = int(channel) - (timestamp, channel, frame) = temp.split() - timestamp = float(timestamp[1:-1]) - (canId, data) = frame.split('#') - if channel.isdigit(): - channel = int(channel) + if len(canId) > 3: + isExtended = True + else: + isExtended = False + canId = int(canId, 16) - if len(canId) > 3: - isExtended = True - else: - isExtended = False - canId = int(canId, 16) - - if data and data[0].lower() == 'r': - isRemoteFrame = True - if len(data) > 1: - dlc = int(data[1:]) - else: - dlc = 0 + if data and data[0].lower() == 'r': + isRemoteFrame = True + if len(data) > 1: + dlc = int(data[1:]) else: - isRemoteFrame = False + dlc = 0 + else: + isRemoteFrame = False - dlc = int(len(data) / 2) - dataBin = bytearray() - for i in range(0, 2 * dlc, 2): - dataBin.append(int(data[i:(i + 2)], 16)) + dlc = len(data) // 2 + dataBin = bytearray() + for i in range(0, len(data), 2): + dataBin.append(int(data[i:(i + 2)], 16)) - if canId & CAN_ERR_FLAG and canId & CAN_ERR_BUSERROR: - msg = Message(timestamp=timestamp, is_error_frame=True) - else: - msg = Message(timestamp=timestamp, arbitration_id=canId & 0x1FFFFFFF, - extended_id=isExtended, is_remote_frame=isRemoteFrame, - dlc=dlc, data=dataBin, channel=channel) - yield msg + if canId & CAN_ERR_FLAG and canId & CAN_ERR_BUSERROR: + msg = Message(timestamp=timestamp, is_error_frame=True) + else: + msg = Message(timestamp=timestamp, arbitration_id=canId & 0x1FFFFFFF, + extended_id=isExtended, is_remote_frame=isRemoteFrame, + dlc=dlc, data=dataBin, channel=channel) + yield msg + self.stop() -class CanutilsLogWriter(Listener): + +class CanutilsLogWriter(BaseIOHandler, Listener): """Logs CAN data to an ASCII log file (.log). This class is is compatible with "candump -L". @@ -86,24 +97,23 @@ class CanutilsLogWriter(Listener): It the first message does not have a timestamp, it is set to zero. """ - def __init__(self, filename, channel="vcan0"): + def __init__(self, file, channel="vcan0", append=False): + """ + :param file: a path-like object or as file-like object to write to + If this is a file-like object, is has to opened in text + write mode, not binary write mode. + :param channel: a default channel to use when the message does not + have a channel set + :param bool append: if set to `True` messages are appended to + the file, else the file is truncated + """ + mode = 'a' if append else 'w' + super(CanutilsLogWriter, self).__init__(file, mode=mode) + self.channel = channel - self.log_file = open(filename, 'w') self.last_timestamp = None - def stop(self): - """Stops logging and closes the file.""" - if self.log_file is not None: - self.log_file.close() - self.log_file = None - else: - log.warn("ignoring attempt to colse a already closed file") - def on_message_received(self, msg): - if self.log_file is None: - log.warn("ignoring write attempt to closed file") - return - # this is the case for the very first message: if self.last_timestamp is None: self.last_timestamp = (msg.timestamp or 0.0) @@ -113,22 +123,21 @@ def on_message_received(self, msg): timestamp = self.last_timestamp else: timestamp = msg.timestamp - + channel = msg.channel if msg.channel is not None else self.channel if msg.is_error_frame: - self.log_file.write("(%f) %s %08X#0000000000000000\n" % (timestamp, channel, CAN_ERR_FLAG | CAN_ERR_BUSERROR)) + self.file.write("(%f) %s %08X#0000000000000000\n" % (timestamp, channel, CAN_ERR_FLAG | CAN_ERR_BUSERROR)) elif msg.is_remote_frame: - data = [] if msg.is_extended_id: - self.log_file.write("(%f) %s %08X#R\n" % (timestamp, channel, msg.arbitration_id)) + self.file.write("(%f) %s %08X#R\n" % (timestamp, channel, msg.arbitration_id)) else: - self.log_file.write("(%f) %s %03X#R\n" % (timestamp, channel, msg.arbitration_id)) + self.file.write("(%f) %s %03X#R\n" % (timestamp, channel, msg.arbitration_id)) else: data = ["{:02X}".format(byte) for byte in msg.data] if msg.is_extended_id: - self.log_file.write("(%f) %s %08X#%s\n" % (timestamp, channel, msg.arbitration_id, ''.join(data))) + self.file.write("(%f) %s %08X#%s\n" % (timestamp, channel, msg.arbitration_id, ''.join(data))) else: - self.log_file.write("(%f) %s %03X#%s\n" % (timestamp, channel, msg.arbitration_id, ''.join(data))) + self.file.write("(%f) %s %03X#%s\n" % (timestamp, channel, msg.arbitration_id, ''.join(data))) diff --git a/can/io/csv.py b/can/io/csv.py index 1933648ac..92b6fb921 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -1,25 +1,27 @@ -#!/usr/bin/env python # coding: utf-8 """ This module contains handling for CSV (comma seperated values) files. TODO: CAN FD messages are not yet supported. + TODO: This module could use https://docs.python.org/2/library/csv.html#module-csv to allow different delimiters for writing, special escape chars to circumvent the base64 encoding and use csv.Sniffer to automatically deduce the delimiters of a CSV file. """ +from __future__ import absolute_import + from base64 import b64encode, b64decode from can.message import Message from can.listener import Listener +from .generic import BaseIOHandler - -class CSVWriter(Listener): +class CSVWriter(BaseIOHandler, Listener): """Writes a comma separated text file with a line for - each message. + each message. Includes a header line. The columns are as follows: @@ -38,42 +40,59 @@ class CSVWriter(Listener): Each line is terminated with a platform specific line seperator. """ - def __init__(self, filename): - self.csv_file = open(filename, 'wt') + def __init__(self, file, append=False): + """ + :param file: a path-like object or as file-like object to write to + If this is a file-like object, is has to opened in text + write mode, not binary write mode. + :param bool append: if set to `True` messages are appended to + the file and no header line is written, else + the file is truncated and starts with a newly + written header line + """ + mode = 'a' if append else 'w' + super(CSVWriter, self).__init__(file, mode=mode) # Write a header row - self.csv_file.write("timestamp,arbitration_id,extended,remote,error,dlc,data\n") + if not append: + self.file.write("timestamp,arbitration_id,extended,remote,error,dlc,data\n") def on_message_received(self, msg): row = ','.join([ repr(msg.timestamp), # cannot use str() here because that is rounding hex(msg.arbitration_id), - '1' if msg.id_type else '0', + '1' if msg.is_extended_id else '0', '1' if msg.is_remote_frame else '0', '1' if msg.is_error_frame else '0', str(msg.dlc), b64encode(msg.data).decode('utf8') ]) - self.csv_file.write(row + '\n') + self.file.write(row) + self.file.write('\n') - def stop(self): - self.csv_file.flush() - self.csv_file.close() -class CSVReader(): +class CSVReader(BaseIOHandler): """Iterator over CAN messages from a .csv file that was generated by :class:`~can.CSVWriter` or that uses the same - format that is described there. + format as described there. Assumes that there is a header + and thus skips the first line. + + Any line seperator is accepted. """ - def __init__(self, filename): - self.csv_file = open(filename, 'rt') + def __init__(self, file): + """ + :param file: a path-like object or as file-like object to read from + If this is a file-like object, is has to opened in text + read mode, not binary read mode. + """ + super(CSVReader, self).__init__(file, mode='r') + def __iter__(self): # skip the header line - self.header_line = next(self.csv_file).split(',') + next(self.file) - def __iter__(self): - for line in self.csv_file: + for line in self.file: timestamp, arbitration_id, extended, remote, error, dlc, data = line.split(',') @@ -87,4 +106,4 @@ def __iter__(self): data=b64decode(data), ) - self.csv_file.close() + self.stop() diff --git a/can/io/generic.py b/can/io/generic.py new file mode 100644 index 000000000..050e9f0e5 --- /dev/null +++ b/can/io/generic.py @@ -0,0 +1,50 @@ +# coding: utf-8 + +""" +Contains a generic class for file IO. +""" + +from abc import ABCMeta, abstractmethod + +from can import Listener + + +class BaseIOHandler(object): + """A generic file handler that can be used for reading and writing. + + Can be used as a context manager. + + :attr file-like file: + the file-like object that is kept internally, or None if none + was opened + """ + + __metaclass__ = ABCMeta + + def __init__(self, file, mode='rt'): + """ + :param file: a path-like object to open a file, a file-like object + to be used as a file or `None` to not use a file at all + :param str mode: the mode that should be used to open the file, see + :func:`builtin.open`, ignored if *file* is `None` + """ + if file is None or (hasattr(file, 'read') and hasattr(file, 'write')): + # file is None or some file-like object + self.file = file + else: + # file is some path-like object + self.file = open(file, mode) + + # for multiple inheritance + super(BaseIOHandler, self).__init__() + + def __enter__(self): + return self + + def __exit__(self, *args): + self.stop() + + def stop(self): + if self.file is not None: + # this also implies a flush() + self.file.close() diff --git a/can/io/logger.py b/can/io/logger.py old mode 100755 new mode 100644 index c4b27815e..cc11579c2 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -1,23 +1,26 @@ -#!/usr/bin/env python # coding: utf-8 """ See the :class:`Logger` class. """ +from __future__ import absolute_import + import logging +from ..listener import Listener +from .generic import BaseIOHandler from .asc import ASCWriter from .blf import BLFWriter from .canutils import CanutilsLogWriter from .csv import CSVWriter from .sqlite import SqliteWriter -from .stdout import Printer +from .printer import Printer log = logging.getLogger("can.io.logger") -class Logger(object): +class Logger(BaseIOHandler, Listener): """ Logs CAN messages to a file. @@ -29,25 +32,32 @@ class Logger(object): * .log :class:`can.CanutilsLogWriter` * other: :class:`can.Printer` - Note this class itself is just a dispatcher, - an object that inherits from Listener will - be created when instantiating this class. + .. note:: + This class itself is just a dispatcher, and any positional an keyword + arguments are passed on to the returned instance. """ @staticmethod - def __new__(cls, filename): - if not filename: - return Printer() - elif filename.endswith(".asc"): - return ASCWriter(filename) - elif filename.endswith(".blf"): - return BLFWriter(filename) - elif filename.endswith(".csv"): - return CSVWriter(filename) - elif filename.endswith(".db"): - return SqliteWriter(filename) - elif filename.endswith(".log"): - return CanutilsLogWriter(filename) - else: - log.info('unknown file type "%s", falling pack to can.Printer', filename) - return Printer(filename) + def __new__(cls, filename, *args, **kwargs): + """ + :type filename: str or None or path-like + :param filename: the filename/path the file to write to, + may be a path-like object if the target logger supports + it, and may be None to instantiate a :class:`~can.Printer` + + """ + if filename: + if filename.endswith(".asc"): + return ASCWriter(filename, *args, **kwargs) + elif filename.endswith(".blf"): + return BLFWriter(filename, *args, **kwargs) + elif filename.endswith(".csv"): + return CSVWriter(filename, *args, **kwargs) + elif filename.endswith(".db"): + return SqliteWriter(filename, *args, **kwargs) + elif filename.endswith(".log"): + return CanutilsLogWriter(filename, *args, **kwargs) + + # else: + log.info('unknown file type "%s", falling pack to can.Printer', filename) + return Printer(filename, *args, **kwargs) diff --git a/can/io/player.py b/can/io/player.py old mode 100755 new mode 100644 index 958f6a8dd..80fa585f0 --- a/can/io/player.py +++ b/can/io/player.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -7,11 +6,12 @@ in the recorded order an time intervals. """ -from __future__ import absolute_import, print_function +from __future__ import absolute_import import time import logging +from .generic import BaseIOHandler from .asc import ASCReader from .blf import BLFReader from .canutils import CanutilsLogReader @@ -21,7 +21,7 @@ log = logging.getLogger('can.io.player') -class LogReader(object): +class LogReader(BaseIOHandler): """ Replay logged CAN messages from a file. @@ -34,29 +34,33 @@ class LogReader(object): Exposes a simple iterator interface, to use simply: - >>> for m in LogReader(my_file): - ... print(m) + >>> for msg in LogReader("some/path/to/my_file.log"): + ... print(msg) .. note:: - There are no time delays, if you want to reproduce - the measured delays between messages look at the - :class:`can.util.MessageSync` class. + There are no time delays, if you want to reproduce the measured + delays between messages look at the :class:`can.MessageSync` class. + + .. note:: + This class itself is just a dispatcher, and any positional an keyword + arguments are passed on to the returned instance. """ @staticmethod - def __new__(cls, filename): - if not filename: - raise TypeError("a filename must be given") - elif filename.endswith(".asc"): - return ASCReader(filename) + def __new__(cls, filename, *args, **kwargs): + """ + :param str filename: the filename/path the file to read from + """ + if filename.endswith(".asc"): + return ASCReader(filename, *args, **kwargs) elif filename.endswith(".blf"): - return BLFReader(filename) + return BLFReader(filename, *args, **kwargs) elif filename.endswith(".csv"): - return CSVReader(filename) + return CSVReader(filename, *args, **kwargs) elif filename.endswith(".db"): - return SqliteReader(filename) + return SqliteReader(filename, *args, **kwargs) elif filename.endswith(".log"): - return CanutilsLogReader(filename) + return CanutilsLogReader(filename, *args, **kwargs) else: raise NotImplementedError("No read support for this log format: {}".format(filename)) @@ -67,12 +71,12 @@ class MessageSync(object): """ def __init__(self, messages, timestamps=True, gap=0.0001, skip=60): - """Creates an new `MessageSync` instance. + """Creates an new **MessageSync** instance. :param messages: An iterable of :class:`can.Message` instances. - :param timestamps: Use the messages' timestamps. - :param gap: Minimum time between sent messages - :param skip: Skip periods of inactivity greater than this. + :param bool timestamps: Use the messages' timestamps. + :param float gap: Minimum time between sent messages in seconds + :param float skip: Skip periods of inactivity greater than this (in seconds). """ self.raw_messages = messages self.timestamps = timestamps diff --git a/can/io/printer.py b/can/io/printer.py new file mode 100644 index 000000000..cb9c4581d --- /dev/null +++ b/can/io/printer.py @@ -0,0 +1,41 @@ +# coding: utf-8 + +""" +This Listener simply prints to stdout / the terminal or a file. +""" + +from __future__ import print_function, absolute_import + +import logging + +from can.listener import Listener +from .generic import BaseIOHandler + +log = logging.getLogger('can.io.printer') + + +class Printer(BaseIOHandler, Listener): + """ + The Printer class is a subclass of :class:`~can.Listener` which simply prints + any messages it receives to the terminal (stdout). A message is tunred into a + string using :meth:`~can.Message.__str__`. + + :attr bool write_to_file: `True` iff this instance prints to a file instead of + standard out + """ + + def __init__(self, file=None): + """ + :param file: an optional path-like object or as file-like object to "print" + to instead of writing to standard out (stdout) + If this is a file-like object, is has to opened in text + write mode, not binary write mode. + """ + self.write_to_file = file is not None + super(Printer, self).__init__(file, mode='w') + + def on_message_received(self, msg): + if self.write_to_file: + self.file.write(str(msg) + '\n') + else: + print(msg) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 5f3255729..3da3cefe5 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -1,12 +1,13 @@ -#!/usr/bin/env python # coding: utf-8 """ Implements an SQL database writer and reader for storing CAN messages. -The database schema is given in the documentation of the loggers. +.. note:: The database schema is given in the documentation of the loggers. """ +from __future__ import absolute_import + import sys import time import threading @@ -15,111 +16,157 @@ from can.listener import BufferedReader from can.message import Message +from .generic import BaseIOHandler -log = logging.getLogger('can.io.sql') +log = logging.getLogger('can.io.sqlite') -# TODO comment on this -if sys.version_info > (3,): - buffer = memoryview +if sys.version_info.major < 3: + # legacy fallback for Python 2 + memoryview = buffer -class SqliteReader: +class SqliteReader(BaseIOHandler): """ Reads recorded CAN messages from a simple SQL database. This class can be iterated over or used to fetch all messages in the database with :meth:`~SqliteReader.read_all`. - Calling len() on this object might not run in constant time. + Calling :func:`~builtin.len` on this object might not run in constant time. + + :attr str table_name: the name of the database table used for storing the messages + + .. note:: The database schema is given in the documentation of the loggers. """ - _SELECT_ALL_COMMAND = "SELECT * FROM messages" + def __init__(self, file, table_name="messages"): + """ + :param file: a `str` or since Python 3.7 a path like object that points + to the database file to use + :param str table_name: the name of the table to look for the messages + + .. warning:: In contrary to all other readers/writers the Sqlite handlers + do not accept file-like objects as the `file` parameter. + It also runs in ``append=True`` mode all the time. + """ + super(SqliteReader, self).__init__(file=None) + self._conn = sqlite3.connect(file) + self._cursor = self._conn.cursor() + self.table_name = table_name - def __init__(self, filename): - log.debug("Starting SqliteReader with %s", filename) - self.conn = sqlite3.connect(filename) - self.cursor = self.conn.cursor() + def __iter__(self): + for frame_data in self._cursor.execute("SELECT * FROM {}".format(self.table_name)): + yield SqliteReader._assemble_message(frame_data) - @staticmethod - def _create_frame_from_db_tuple(frame_data): + @staticmethod + def _assemble_message(frame_data): timestamp, can_id, is_extended, is_remote, is_error, dlc, data = frame_data return Message( - timestamp, is_remote, is_extended, is_error, can_id, dlc, data + timestamp=timestamp, + is_remote_frame=bool(is_remote), + extended_id=bool(is_extended), + is_error_frame=bool(is_error), + arbitration_id=can_id, + dlc=dlc, + data=data ) - def __iter__(self): - log.debug("Iterating through messages from sql db") - for frame_data in self.cursor.execute(self._SELECT_ALL_COMMAND): - yield self._create_frame_from_db_tuple(frame_data) - def __len__(self): # this might not run in constant time - result = self.cursor.execute("SELECT COUNT(*) FROM messages") + result = self._cursor.execute("SELECT COUNT(*) FROM {}".format(self.table_name)) return int(result.fetchone()[0]) def read_all(self): - """Fetches all messages in the database.""" - result = self.cursor.execute(self._SELECT_ALL_COMMAND) - return result.fetchall() + """Fetches all messages in the database. + + :rtype: Generator[can.Message] + """ + result = self._cursor.execute("SELECT * FROM {}".format(self.table_name)).fetchall() + return (SqliteReader._assemble_message(frame) for frame in result) - def close(self): - """Closes the connection to the database.""" - self.conn.close() + def stop(self): + """Closes the connection to the database. + """ + super(SqliteReader, self).stop() + self._conn.close() -class SqliteWriter(BufferedReader): +class SqliteWriter(BaseIOHandler, BufferedReader): """Logs received CAN data to a simple SQL database. The sqlite database may already exist, otherwise it will be created when the first message arrives. Messages are internally buffered and written to the SQL file in a background - thread. + thread. Ensures that all messages that are added before calling :meth:`~can.SqliteWriter.stop()` + are actually written to the database after that call returns. Thus, calling + :meth:`~can.SqliteWriter.stop()` may take a while. + + :attr str table_name: the name of the database table used for storing the messages + :attr int num_frames: the number of frames actally writtem to the database, this + excludes messages that are still buffered + :attr float last_write: the last time a message war actually written to the database, + as given by ``time.time()`` .. note:: When the listener's :meth:`~SqliteWriter.stop` method is called the - thread writing to the sql file will continue to receive and internally + thread writing to the database will continue to receive and internally buffer messages if they continue to arrive before the :attr:`~SqliteWriter.GET_MESSAGE_TIMEOUT`. If the :attr:`~SqliteWriter.GET_MESSAGE_TIMEOUT` expires before a message - is received, the internal buffer is written out to the sql file. + is received, the internal buffer is written out to the database file. However if the bus is still saturated with messages, the Listener - will continue receiving until the :attr:`~SqliteWriter.MAX_TIME_BETWEEN_WRITES` - timeout is reached. + will continue receiving until the :attr:`~can.SqliteWriter.MAX_TIME_BETWEEN_WRITES` + timeout is reached or more than + :attr:`~can.SqliteWriter.MAX_BUFFER_SIZE_BEFORE_WRITES` messages are buffered. - """ + .. note:: The database schema is given in the documentation of the loggers. - _INSERT_MSG_TEMPLATE = ''' - INSERT INTO messages VALUES - (?, ?, ?, ?, ?, ?, ?) - ''' + """ GET_MESSAGE_TIMEOUT = 0.25 """Number of seconds to wait for messages from internal queue""" - MAX_TIME_BETWEEN_WRITES = 5 + MAX_TIME_BETWEEN_WRITES = 5.0 """Maximum number of seconds to wait between writes to the database""" - def __init__(self, filename): - super(SqliteWriter, self).__init__() - self.db_fn = filename - self.stop_running_event = threading.Event() - self.writer_thread = threading.Thread(target=self._db_writer_thread) - self.writer_thread.start() + MAX_BUFFER_SIZE_BEFORE_WRITES = 500 + """Maximum number of messages to buffer before writing to the database""" + + def __init__(self, file, table_name="messages"): + """ + :param file: a `str` or since Python 3.7 a path like object that points + to the database file to use + :param str table_name: the name of the table to store messages in + + .. warning:: In contrary to all other readers/writers the Sqlite handlers + do not accept file-like objects as the `file` parameter. + """ + super(SqliteWriter, self).__init__(file=None) + self.table_name = table_name + self._db_filename = file + self._stop_running_event = threading.Event() + self._writer_thread = threading.Thread(target=self._db_writer_thread) + self._writer_thread.start() + self.num_frames = 0 + self.last_write = time.time() def _create_db(self): - # Note: you can't share sqlite3 connections between threads - # hence we setup the db here. - log.info("Creating sqlite database") - self.conn = sqlite3.connect(self.db_fn) - cursor = self.conn.cursor() + """Creates a new databae or opens a connection to an existing one. + + .. note:: + You can't share sqlite3 connections between threads (by default) + hence we setup the db here. It has the upside of running async. + """ + log.debug("Creating sqlite database") + self._conn = sqlite3.connect(self._db_filename) # create table structure - cursor.execute(''' - CREATE TABLE IF NOT EXISTS messages + self._conn.cursor().execute(""" + CREATE TABLE IF NOT EXISTS {} ( ts REAL, arbitration_id INTEGER, @@ -129,52 +176,61 @@ def _create_db(self): dlc INTEGER, data BLOB ) - ''') - self.conn.commit() + """.format(self.table_name)) + self._conn.commit() + + self._insert_template = "INSERT INTO {} VALUES (?, ?, ?, ?, ?, ?, ?)".format(self.table_name) def _db_writer_thread(self): - num_frames = 0 - last_write = time.time() self._create_db() - while not self.stop_running_event.is_set(): - messages = [] - - msg = self.get_message(self.GET_MESSAGE_TIMEOUT) - while msg is not None: - log.debug("SqliteWriter: buffering message") - - messages.append(( - msg.timestamp, - msg.arbitration_id, - msg.id_type, - msg.is_remote_frame, - msg.is_error_frame, - msg.dlc, - buffer(msg.data) - )) - - if time.time() - last_write > self.MAX_TIME_BETWEEN_WRITES: - log.debug("Max timeout between writes reached") - break + try: + while True: + messages = [] # reset buffer msg = self.get_message(self.GET_MESSAGE_TIMEOUT) + while msg is not None: + #log.debug("SqliteWriter: buffering message") + + messages.append(( + msg.timestamp, + msg.arbitration_id, + msg.is_extended_id, + msg.is_remote_frame, + msg.is_error_frame, + msg.dlc, + memoryview(msg.data) + )) + + if time.time() - self.last_write > self.MAX_TIME_BETWEEN_WRITES or \ + len(messages) > self.MAX_BUFFER_SIZE_BEFORE_WRITES: + break + else: + # just go on + msg = self.get_message(self.GET_MESSAGE_TIMEOUT) + + count = len(messages) + if count > 0: + with self._conn: + #log.debug("Writing %d frames to db", count) + self._conn.executemany(self._insert_template, messages) + self._conn.commit() # make the changes visible to the entire database + self.num_frames += count + self.last_write = time.time() + + # check if we are still supposed to run and go back up if yes + if self._stop_running_event.is_set(): + break - count = len(messages) - if count > 0: - with self.conn: - log.debug("Writing %s frames to db", count) - self.conn.executemany(SqliteWriter._INSERT_MSG_TEMPLATE, messages) - self.conn.commit() # make the changes visible to the entire database - num_frames += count - last_write = time.time() - - # go back up and check if we are still supposed to run - - self.conn.close() - log.info("Stopped sqlite writer after writing %s messages", num_frames) + finally: + self._conn.close() + log.info("Stopped sqlite writer after writing %d messages", self.num_frames) def stop(self): - self.stop_running_event.set() - log.debug("Stopping sqlite writer") - self.writer_thread.join() + """Stops the reader an writes all remaining messages to the database. Thus, this + might take a while an block. + """ + BufferedReader.stop(self) + self._stop_running_event.set() + self._writer_thread.join() + BaseIOHandler.stop(self) diff --git a/can/io/stdout.py b/can/io/stdout.py deleted file mode 100644 index c0a82ab5a..000000000 --- a/can/io/stdout.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -""" -This Listener simply prints to stdout / the terminal or a file. -""" - -from __future__ import print_function - -import logging - -from can.listener import Listener - -log = logging.getLogger('can.io.stdout') - - -class Printer(Listener): - """ - The Printer class is a subclass of :class:`~can.Listener` which simply prints - any messages it receives to the terminal (stdout). - - :param output_file: An optional file to "print" to. - """ - - def __init__(self, output_file=None): - if output_file is not None: - log.info('Creating log file "{}"'.format(output_file)) - output_file = open(output_file, 'wt') - self.output_file = output_file - - def on_message_received(self, msg): - if self.output_file is not None: - self.output_file.write(str(msg) + '\n') - else: - print(msg) - - def stop(self): - if self.output_file: - self.output_file.write('\n') - self.output_file.close() diff --git a/can/listener.py b/can/listener.py index d7e8e7ced..6ed04b64b 100644 --- a/can/listener.py +++ b/can/listener.py @@ -1,29 +1,62 @@ -#!/usr/bin/env python # coding: utf-8 """ This module contains the implementation of `can.Listener` and some readers. """ +from abc import ABCMeta, abstractmethod + +try: + # Python 3.7 + from queue import SimpleQueue, Empty +except ImportError: + try: + # Python 3.0 - 3.6 + from queue import Queue as SimpleQueue, Empty + except ImportError: + # Python 2 + from Queue import Queue as SimpleQueue, Empty + try: - # Python 3 - import queue + import asyncio except ImportError: - # Python 2 - import Queue as queue + asyncio = None class Listener(object): + """The basic listener that can be called directly to handle some + CAN message:: + + listener = SomeListener() + msg = my_bus.recv() + + # now either call + listener(msg) + # or + listener.on_message_received(msg) + + """ + + __metaclass__ = ABCMeta + @abstractmethod def on_message_received(self, msg): - raise NotImplementedError( - "{} has not implemented on_message_received".format( - self.__class__.__name__) - ) + """This method is called to handle the given message. + + :param can.Message msg: the delivered message + + """ + pass def __call__(self, msg): return self.on_message_received(msg) + def on_error(self, exc): + """This method is called to handle any exception in the receive thread. + + :param Exception exc: The exception causing the thread to stop + """ + def stop(self): """ Override to cleanup any open resources. @@ -32,8 +65,7 @@ def stop(self): class RedirectReader(Listener): """ - A RedirectReader sends all received messages - to another Bus. + A RedirectReader sends all received messages to another Bus. """ @@ -49,25 +81,89 @@ class BufferedReader(Listener): A BufferedReader is a subclass of :class:`~can.Listener` which implements a **message buffer**: that is, when the :class:`can.BufferedReader` instance is notified of a new message it pushes it into a queue of messages waiting to - be serviced. + be serviced. The messages can then be fetched with + :meth:`~can.BufferedReader.get_message`. + + Putting in messages after :meth:`~can.BufferedReader.stop` has be called will raise + an exception, see :meth:`~can.BufferedReader.on_message_received`. + + :attr bool is_stopped: ``True`` iff the reader has been stopped """ def __init__(self): - self.buffer = queue.Queue(0) + # set to "infinite" size + self.buffer = SimpleQueue() + self.is_stopped = False def on_message_received(self, msg): - self.buffer.put(msg) + """Append a message to the buffer. + + :raises: BufferError + if the reader has already been stopped + """ + if self.is_stopped: + raise RuntimeError("reader has already been stopped") + else: + self.buffer.put(msg) def get_message(self, timeout=0.5): """ Attempts to retrieve the latest message received by the instance. If no message is - available it blocks for given timeout or until a message is received (whichever - is shorter), + available it blocks for given timeout or until a message is received, or else + returns None (whichever is shorter). This method does not block after + :meth:`can.BufferedReader.stop` has been called. :param float timeout: The number of seconds to wait for a new message. - :return: the :class:`~can.Message` if there is one, or None if there is not. + :rytpe: can.Message or None + :return: the message if there is one, or None if there is not. """ try: - return self.buffer.get(block=True, timeout=timeout) - except queue.Empty: + return self.buffer.get(block=not self.is_stopped, timeout=timeout) + except Empty: return None + + def stop(self): + """Prohibits any more additions to this reader. + """ + self.is_stopped = True + + +if asyncio is not None: + class AsyncBufferedReader(Listener): + """A message buffer for use with :mod:`asyncio`. + + See :ref:`asyncio` for how to use with :class:`can.Notifier`. + + Can also be used as an asynchronous iterator:: + + async for msg in reader: + print(msg) + """ + + def __init__(self, loop=None): + # set to "infinite" size + self.buffer = asyncio.Queue(loop=loop) + + def on_message_received(self, msg): + """Append a message to the buffer. + + Must only be called inside an event loop! + """ + self.buffer.put_nowait(msg) + + def get_message(self): + """ + Retrieve the latest message when awaited for:: + + msg = await reader.get_message() + + :rtype: can.Message + :return: The CAN message. + """ + return self.buffer.get() + + def __aiter__(self): + return self + + def __anext__(self): + return self.buffer.get() diff --git a/can/logger.py b/can/logger.py index 75a52272f..6da89de96 100644 --- a/can/logger.py +++ b/can/logger.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -17,24 +16,24 @@ Dynamic Controls 2010 """ -from __future__ import print_function +from __future__ import absolute_import, print_function -import datetime +import sys import argparse import socket +from datetime import datetime import can -from can.bus import BusState -from can.io.logger import Logger +from can import Bus, BusState, Logger def main(): parser = argparse.ArgumentParser( "python -m can.logger", - description="Log CAN traffic, printing messages to stdout or to a given file") + description="Log CAN traffic, printing messages to stdout or to a given file.") parser.add_argument("-f", "--file_name", dest="log_file", - help="""Path and base log filename, extension can be .txt, .asc, .csv, .db, .npz""", + help="""Path and base log filename, for supported types see can.Logger.""", default=None) parser.add_argument("-v", action="count", dest="verbosity", @@ -59,8 +58,16 @@ def main(): help='''Bitrate to use for the CAN bus.''') group = parser.add_mutually_exclusive_group(required=False) - group.add_argument('--active', action='store_true') - group.add_argument('--passive', action='store_true') + group.add_argument('--active', help="Start the bus as active, this is applied the default.", + action='store_true') + group.add_argument('--passive', help="Start the bus as passive.", + action='store_true') + + # print help message when no arguments wre given + if len(sys.argv) < 2: + parser.print_help(sys.stderr) + import errno + raise SystemExit(errno.EINVAL) results = parser.parse_args() @@ -84,10 +91,10 @@ def main(): config = {"can_filters": can_filters, "single_handle": True} if results.interface: - config["bustype"] = results.interface + config["interface"] = results.interface if results.bitrate: config["bitrate"] = results.bitrate - bus = can.interface.Bus(results.channel, **config) + bus = Bus(results.channel, **config) if results.active: bus.state = BusState.ACTIVE @@ -96,7 +103,7 @@ def main(): bus.state = BusState.PASSIVE print('Connected to {}: {}'.format(bus.__class__.__name__, bus.channel_info)) - print('Can Logger (Started on {})\n'.format(datetime.datetime.now())) + print('Can Logger (Started on {})\n'.format(datetime.now())) logger = Logger(results.log_file) try: diff --git a/can/message.py b/can/message.py index 9154bc0b5..673bc3ca2 100644 --- a/can/message.py +++ b/can/message.py @@ -1,46 +1,115 @@ -#!/usr/bin/env python # coding: utf-8 """ -This module contains the implementation of `can.Message`. +This module contains the implementation of :class:`can.Message`. + +.. note:: + Could use `@dataclass `__ + starting with Python 3.7. """ -import logging -logger = logging.getLogger(__name__) +from __future__ import absolute_import, division + +import warnings class Message(object): """ The :class:`~can.Message` object is used to represent CAN messages for - both sending and receiving. + sending, receiving and other purposes like converting between different + logging formats. Messages can use extended identifiers, be remote or error frames, contain - data and can be associated to a channel. - - When testing for equality of the messages, the timestamp and the channel - is not used for comparing. + data and may be associated to a channel. - .. note:: + Messages are always compared by identity and never by value, because that + may introduce unexpected behaviour. See also :meth:`~can.Message.equals`. - This class does not strictly check the input. Thus, the caller must - prevent the creation of invalid messages. Possible problems include - the `dlc` field not matching the length of `data` or creating a message - with both `is_remote_frame` and `is_error_frame` set to True. + :func:`~copy.copy`/:func:`~copy.deepcopy` is supported as well. + Messages do not support "dynamic" attributes, meaning any others that the + documented ones. """ - def __init__(self, timestamp=0.0, is_remote_frame=False, extended_id=True, - is_error_frame=False, arbitration_id=0, dlc=None, data=None, + __slots__ = ( + "timestamp", + "arbitration_id", + "is_extended_id", + "is_remote_frame", + "is_error_frame", + "channel", + "dlc", + "data", + "is_fd", + "bitrate_switch", + "error_state_indicator", + "__weakref__", # support weak references to messages + "_dict" # see __getattr__ + ) + + def __getattr__(self, key): + # TODO keep this for a version, in order to not break old code + # this entire method (as well as the _dict attribute in __slots__ and the __setattr__ method) + # can be removed in 4.0 + # this method is only called if the attribute was not found elsewhere, like in __slots__ + try: + warnings.warn("Custom attributes of messages are deprecated and will be removed in the next major version", DeprecationWarning) + return self._dict[key] + except KeyError: + raise AttributeError("'message' object has no attribute '{}'".format(key)) + + def __setattr__(self, key, value): + # see __getattr__ + try: + super(Message, self).__setattr__(key, value) + except AttributeError: + warnings.warn("Custom attributes of messages are deprecated and will be removed in the next major version", DeprecationWarning) + self._dict[key] = value + + @property + def id_type(self): + # TODO remove in 4.0 + warnings.warn("Message.id_type is deprecated, use is_extended_id", DeprecationWarning) + return self.is_extended_id + + @id_type.setter + def id_type(self, value): + # TODO remove in 4.0 + warnings.warn("Message.id_type is deprecated, use is_extended_id", DeprecationWarning) + self.is_extended_id = value + + def __init__(self, timestamp=0.0, arbitration_id=0, is_extended_id=None, + is_remote_frame=False, is_error_frame=False, channel=None, + dlc=None, data=None, is_fd=False, bitrate_switch=False, error_state_indicator=False, - channel=None): + extended_id=True, + check=False): + """ + To create a message object, simply provide any of the below attributes + together with additional parameters as keyword arguments to the constructor. + + :param bool check: By default, the constructor of this class does not strictly check the input. + Thus, the caller must prevent the creation of invalid messages or + set this parameter to `True`, to raise an Error on invalid inputs. + Possible problems include the `dlc` field not matching the length of `data` + or creating a message with both `is_remote_frame` and `is_error_frame` set to `True`. + + :raises ValueError: iff `check` is set to `True` and one or more arguments were invalid + """ + self._dict = dict() # see __getattr__ self.timestamp = timestamp - self.id_type = extended_id - self.is_extended_id = extended_id + self.arbitration_id = arbitration_id + if is_extended_id is not None: + self.is_extended_id = is_extended_id + else: + if not extended_id: + # Passed extended_id=False (default argument is True) so we warn to update + warnings.warn("extended_id is a deprecated parameter, use is_extended_id", DeprecationWarning) + self.is_extended_id = extended_id self.is_remote_frame = is_remote_frame self.is_error_frame = is_error_frame - self.arbitration_id = arbitration_id self.channel = channel self.is_fd = is_fd @@ -63,35 +132,34 @@ def __init__(self, timestamp=0.0, is_remote_frame=False, extended_id=True, else: self.dlc = dlc - if is_fd and self.dlc > 64: - logger.warning("data link count was %d but it should be less than or equal to 64", self.dlc) - if not is_fd and self.dlc > 8: - logger.warning("data link count was %d but it should be less than or equal to 8", self.dlc) + if check: + self._check() def __str__(self): - field_strings = ["Timestamp: {0:15.6f}".format(self.timestamp)] - if self.id_type: - # Extended arbitrationID + field_strings = ["Timestamp: {0:>15.6f}".format(self.timestamp)] + if self.is_extended_id: arbitration_id_string = "ID: {0:08x}".format(self.arbitration_id) else: arbitration_id_string = "ID: {0:04x}".format(self.arbitration_id) field_strings.append(arbitration_id_string.rjust(12, " ")) flag_string = " ".join([ - "X" if self.id_type else "S", + "X" if self.is_extended_id else "S", "E" if self.is_error_frame else " ", "R" if self.is_remote_frame else " ", "F" if self.is_fd else " ", + "BS" if self.bitrate_switch else " ", + "EI" if self.error_state_indicator else " " ]) field_strings.append(flag_string) - field_strings.append("DLC: {0:d}".format(self.dlc)) + field_strings.append("DLC: {0:2d}".format(self.dlc)) data_strings = [] if self.data is not None: for index in range(0, min(self.dlc, len(self.data))): data_strings.append("{0:02x}".format(self.data[index])) - if data_strings: # if not empty + if data_strings: # if not empty field_strings.append(" ".join(data_strings).ljust(24, " ")) else: field_strings.append(" " * 24) @@ -102,58 +170,155 @@ def __str__(self): except UnicodeError: pass + if self.channel is not None: + field_strings.append("Channel: {}".format(self.channel)) + return " ".join(field_strings).strip() def __len__(self): return len(self.data) def __bool__(self): + # For Python 3 return True def __nonzero__(self): + # For Python 2 return self.__bool__() def __repr__(self): - data = ["{:#02x}".format(byte) for byte in self.data] args = ["timestamp={}".format(self.timestamp), - "is_remote_frame={}".format(self.is_remote_frame), - "extended_id={}".format(self.id_type), - "is_error_frame={}".format(self.is_error_frame), "arbitration_id={:#x}".format(self.arbitration_id), - "dlc={}".format(self.dlc), - "data=[{}]".format(", ".join(data))] + "extended_id={}".format(self.is_extended_id)] + + if self.is_remote_frame: + args.append("is_remote_frame={}".format(self.is_remote_frame)) + + if self.is_error_frame: + args.append("is_error_frame={}".format(self.is_error_frame)) + if self.channel is not None: - args.append("channel={!r}".format(self.channel)) + args.append("channel={!r}".format(self.channel)) + + data = ["{:#02x}".format(byte) for byte in self.data] + args += ["dlc={}".format(self.dlc), + "data=[{}]".format(", ".join(data))] + if self.is_fd: args.append("is_fd=True") args.append("bitrate_switch={}".format(self.bitrate_switch)) args.append("error_state_indicator={}".format(self.error_state_indicator)) + return "can.Message({})".format(", ".join(args)) - def __eq__(self, other): - return (isinstance(other, self.__class__) and + def __format__(self, format_spec): + if not format_spec: + return self.__str__() + else: + raise ValueError("non empty format_specs are not supported") + + def __bytes__(self): + return bytes(self.data) + + def __copy__(self): + new = Message( + timestamp=self.timestamp, + arbitration_id=self.arbitration_id, + extended_id=self.is_extended_id, + is_remote_frame=self.is_remote_frame, + is_error_frame=self.is_error_frame, + channel=self.channel, + dlc=self.dlc, + data=self.data, + is_fd=self.is_fd, + bitrate_switch=self.bitrate_switch, + error_state_indicator=self.error_state_indicator + ) + new._dict.update(self._dict) + return new + + def __deepcopy__(self, memo): + new = Message( + timestamp=self.timestamp, + arbitration_id=self.arbitration_id, + extended_id=self.is_extended_id, + is_remote_frame=self.is_remote_frame, + is_error_frame=self.is_error_frame, + channel=deepcopy(self.channel, memo), + dlc=self.dlc, + data=deepcopy(self.data, memo), + is_fd=self.is_fd, + bitrate_switch=self.bitrate_switch, + error_state_indicator=self.error_state_indicator + ) + new._dict.update(self._dict) + return new + + def _check(self): + """Checks if the message parameters are valid. + Assumes that the types are already correct. + + :raises AssertionError: iff one or more attributes are invalid + """ + + assert 0.0 <= self.timestamp, "the timestamp may not be negative" + + assert not (self.is_remote_frame and self.is_error_frame), \ + "a message cannot be a remote and an error frame at the sane time" + + assert 0 <= self.arbitration_id, "arbitration IDs may not be negative" + + if self.is_extended_id: + assert self.arbitration_id < 0x20000000, "Extended arbitration IDs must be less than 2^29" + else: + assert self.arbitration_id < 0x800, "Normal arbitration IDs must be less than 2^11" + + assert 0 <= self.dlc, "DLC may not be negative" + if self.is_fd: + assert self.dlc <= 64, "DLC was {} but it should be <= 64 for CAN FD frames".format(self.dlc) + else: + assert self.dlc <= 8, "DLC was {} but it should be <= 8 for normal CAN frames".format(self.dlc) + + if not self.is_remote_frame: + assert self.dlc == len(self.data), "the length of the DLC and the length of the data must match up" + + if not self.is_fd: + assert not self.bitrate_switch, "bitrate switch is only allowed for CAN FD frames" + assert not self.error_state_indicator, "error stat indicator is only allowed for CAN FD frames" + + def equals(self, other, timestamp_delta=1.0e-6): + """ + Compares a given message with this one. + + :param can.Message other: the message to compare with + + :type timestamp_delta: float or int or None + :param timestamp_delta: the maximum difference at which two timestamps are + still considered equal or None to not compare timestamps + + :rtype: bool + :return: True iff the given message equals this one + """ + # see https://github.com/hardbyte/python-can/pull/413 for a discussion + # on why a delta of 1.0e-6 was chosen + return ( + # check for identity first + self is other or + # then check for equality by value + ( + ( + timestamp_delta is None or + abs(self.timestamp - other.timestamp) <= timestamp_delta + ) and self.arbitration_id == other.arbitration_id and - #self.timestamp == other.timestamp and # allow the timestamp to differ - self.id_type == other.id_type and + self.is_extended_id == other.is_extended_id and self.dlc == other.dlc and self.data == other.data and self.is_remote_frame == other.is_remote_frame and self.is_error_frame == other.is_error_frame and + self.channel == other.channel and self.is_fd == other.is_fd and - self.bitrate_switch == other.bitrate_switch) - - def __hash__(self): - return hash(( - self.arbitration_id, - # self.timestamp # excluded, like in self.__eq__(self, other) - self.id_type, - self.dlc, - self.data, - self.is_fd, - self.bitrate_switch, - self.is_remote_frame, - self.is_error_frame - )) - - def __format__(self, format_spec): - return self.__str__() + self.bitrate_switch == other.bitrate_switch and + self.error_state_indicator == other.error_state_indicator + ) + ) diff --git a/can/notifier.py b/can/notifier.py index 650353694..45df675dd 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -8,23 +7,30 @@ import threading import logging import time +try: + import asyncio +except ImportError: + asyncio = None logger = logging.getLogger('can.Notifier') class Notifier(object): - def __init__(self, bus, listeners, timeout=1): + def __init__(self, bus, listeners, timeout=1.0, loop=None): """Manages the distribution of **Messages** from a given bus/buses to a list of listeners. - :param can.BusABC bus: The :ref:`bus` or a list of buses to listen to. + :param can.BusABC bus: A :ref:`bus` or a list of buses to listen to. :param list listeners: An iterable of :class:`~can.Listener` :param float timeout: An optional maximum number of seconds to wait for any message. + :param asyncio.AbstractEventLoop loop: + An :mod:`asyncio` event loop to schedule listeners in. """ self.listeners = listeners self.bus = bus self.timeout = timeout + self._loop = loop #: Exception raised in thread self.exception = None @@ -35,11 +41,24 @@ def __init__(self, bus, listeners, timeout=1): self._readers = [] buses = self.bus if isinstance(self.bus, list) else [self.bus] for bus in buses: + self.add_bus(bus) + + def add_bus(self, bus): + """Add a bus for notification. + + :param can.BusABC bus: + CAN bus instance. + """ + if self._loop is not None and hasattr(bus, 'fileno') and bus.fileno() >= 0: + # Use file descriptor to watch for messages + reader = bus.fileno() + self._loop.add_reader(reader, self._on_message_available, bus) + else: reader = threading.Thread(target=self._rx_thread, args=(bus,), - name='can.notifier for bus "{}"'.format(bus.channel_info)) + name='can.notifier for bus "{}"'.format(bus.channel_info)) reader.daemon = True reader.start() - self._readers.append(reader) + self._readers.append(reader) def stop(self, timeout=5): """Stop notifying Listeners when new :class:`~can.Message` objects arrive @@ -52,11 +71,16 @@ def stop(self, timeout=5): self._running = False end_time = time.time() + timeout for reader in self._readers: - now = time.time() - if now < end_time: - reader.join(end_time - now) + if isinstance(reader, threading.Thread): + now = time.time() + if now < end_time: + reader.join(end_time - now) + else: + # reader is a file descriptor + self._loop.remove_reader(reader) for listener in self.listeners: - listener.stop() + if hasattr(listener, 'stop'): + listener.stop() def _rx_thread(self, bus): msg = None @@ -64,13 +88,37 @@ def _rx_thread(self, bus): while self._running: if msg is not None: with self._lock: - for callback in self.listeners: - callback(msg) + if self._loop is not None: + self._loop.call_soon_threadsafe( + self._on_message_received, msg) + else: + self._on_message_received(msg) msg = bus.recv(self.timeout) except Exception as exc: self.exception = exc + if self._loop is not None: + self._loop.call_soon_threadsafe(self._on_error, exc) + else: + self._on_error(exc) raise + def _on_message_available(self, bus): + msg = bus.recv(0) + if msg is not None: + self._on_message_received(msg) + + def _on_message_received(self, msg): + for callback in self.listeners: + res = callback(msg) + if self._loop is not None and asyncio.iscoroutine(res): + # Schedule coroutine + self._loop.create_task(res) + + def _on_error(self, exc): + for listener in self.listeners: + if hasattr(listener, 'on_error'): + listener.on_error(exc) + def add_listener(self, listener): """Add new Listener to the notification list. If it is already present, it will be called two times diff --git a/can/player.py b/can/player.py index 984c971f9..c712f1714 100644 --- a/can/player.py +++ b/can/player.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -8,22 +7,23 @@ Similar to canplayer in the can-utils package. """ -from __future__ import print_function +from __future__ import absolute_import, print_function +import sys import argparse -import datetime +from datetime import datetime import can -from can.io.player import LogReader, MessageSync +from can import Bus, LogReader, MessageSync def main(): parser = argparse.ArgumentParser( "python -m can.player", - description="Replay CAN traffic") + description="Replay CAN traffic.") parser.add_argument("-f", "--file_name", dest="log_file", - help="""Path and base log filename, extension can be .txt, .asc, .csv, .db, .npz""", + help="""Path and base log filename, for supported types see can.LogReader.""", default=None) parser.add_argument("-v", action="count", dest="verbosity", @@ -44,38 +44,43 @@ def main(): help='''Bitrate to use for the CAN bus.''') parser.add_argument('--ignore-timestamps', dest='timestamps', - help='''Ignore timestamps (send all frames immediately with minimum gap between - frames)''', action='store_false') + help='''Ignore timestamps (send all frames immediately with minimum gap between frames)''', + action='store_false') - parser.add_argument('-g', '--gap', type=float, help=''' minimum time between replayed frames''') + parser.add_argument('-g', '--gap', type=float, help=''' minimum time between replayed frames''', + default=0.0001) parser.add_argument('-s', '--skip', type=float, default=60*60*24, help=''' skip gaps greater than 's' seconds''') parser.add_argument('infile', metavar='input-file', type=str, - help='The file to replay. Supported types: .db, .blf') + help='The file to replay. For supported types see can.LogReader.') + + # print help message when no arguments were given + if len(sys.argv) < 2: + parser.print_help(sys.stderr) + import errno + raise SystemExit(errno.EINVAL) results = parser.parse_args() verbosity = results.verbosity - gap = 0.0001 if results.gap is None else results.gap logging_level_name = ['critical', 'error', 'warning', 'info', 'debug', 'subdebug'][min(5, verbosity)] can.set_logging_level(logging_level_name) config = {"single_handle": True} if results.interface: - config["bustype"] = results.interface + config["interface"] = results.interface if results.bitrate: config["bitrate"] = results.bitrate - bus = can.interface.Bus(results.channel, **config) + bus = Bus(results.channel, **config) - player = LogReader(results.infile) + reader = LogReader(results.infile) - in_sync = MessageSync(player, timestamps=results.timestamps, - gap=gap, skip=results.skip) + in_sync = MessageSync(reader, timestamps=results.timestamps, + gap=results.gap, skip=results.skip) - print('Can LogReader (Started on {})'.format( - datetime.datetime.now())) + print('Can LogReader (Started on {})'.format(datetime.now())) try: for m in in_sync: @@ -86,6 +91,7 @@ def main(): pass finally: bus.shutdown() + reader.stop() if __name__ == "__main__": diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 3a126d90e..c7d458366 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -1,7 +1,7 @@ -#!/usr/bin/env python # coding: utf-8 from __future__ import print_function, absolute_import + from threading import RLock try: @@ -16,19 +16,23 @@ from .interface import Bus -class NullContextManager(object): - """ - A context manager that does nothing at all. - """ +try: + from contextlib import nullcontext - def __init__(self, resource=None): - self.resource = resource +except ImportError: + class nullcontext(object): + """A context manager that does nothing at all. + A fallback for Python 3.7's :class:`contextlib.nullcontext` manager. + """ - def __enter__(self): - return self.resource + def __init__(self, enter_result=None): + self.enter_result = enter_result - def __exit__(self, *args): - pass + def __enter__(self): + return self.enter_result + + def __exit__(self, *args): + pass class ThreadSafeBus(ObjectProxy): @@ -48,10 +52,6 @@ class ThreadSafeBus(ObjectProxy): instead of :meth:`~can.BusABC.recv` directly. """ - # init locks for sending and receiving separately - _lock_send = RLock() - _lock_recv = RLock() - def __init__(self, *args, **kwargs): if import_exc is not None: raise import_exc @@ -60,7 +60,11 @@ def __init__(self, *args, **kwargs): # now, BusABC.send_periodic() does not need a lock anymore, but the # implementation still requires a context manager - self.__wrapped__._lock_send_periodic = NullContextManager() + self.__wrapped__._lock_send_periodic = nullcontext() + + # init locks for sending and receiving separately + self._lock_send = RLock() + self._lock_recv = RLock() def recv(self, timeout=None, *args, **kwargs): with self._lock_recv: diff --git a/can/util.py b/can/util.py index a471fda89..af421651f 100644 --- a/can/util.py +++ b/can/util.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ @@ -13,6 +12,8 @@ import platform import re import logging +import warnings + try: from configparser import ConfigParser except ImportError: @@ -54,7 +55,7 @@ ) -def load_file_config(path=None): +def load_file_config(path=None, section=None): """ Loads configuration from file with following content:: @@ -65,7 +66,8 @@ def load_file_config(path=None): :param path: path to config file. If not specified, several sensible default locations are tried depending on platform. - + :param section: + name of the section to read configuration from. """ config = ConfigParser() if path is None: @@ -73,13 +75,16 @@ def load_file_config(path=None): else: config.read(path) - if not config.has_section('default'): - return {} + _config = {} - return dict( - (key, val) - for key, val in config.items('default') - ) + section = section if section is not None else 'default' + if config.has_section(section): + if config.has_section('default'): + _config.update( + dict((key, val) for key, val in config.items('default'))) + _config.update(dict((key, val) for key, val in config.items(section))) + + return _config def load_environment_config(): @@ -103,7 +108,7 @@ def load_environment_config(): ) -def load_config(path=None, config=None): +def load_config(path=None, config=None, context=None): """ Returns a dict with configuration details which is loaded from (in this order): @@ -128,6 +133,10 @@ def load_config(path=None, config=None): A dict which may set the 'interface', and/or the 'channel', or neither. It may set other values that are passed through. + :param context: + Extra 'context' pass to config sources. This can be use to section + other than 'default' in the configuration file. + :return: A config dictionary that should contain 'interface' & 'channel':: @@ -147,21 +156,21 @@ def load_config(path=None, config=None): """ # start with an empty dict to apply filtering to all sources - given_config = config + given_config = config or {} config = {} # use the given dict for default values config_sources = [ given_config, can.rc, - load_environment_config, - lambda: load_file_config(path) + lambda _context: load_environment_config(), # context is not supported + lambda _context: load_file_config(path, _context) ] # Slightly complex here to only search for the file config if required for cfg in config_sources: if callable(cfg): - cfg = cfg() + cfg = cfg(context) # remove legacy operator (and copy to interface if not already present) if 'bustype' in cfg: if 'interface' not in cfg or not cfg['interface']: @@ -177,11 +186,11 @@ def load_config(path=None, config=None): if key not in config: config[key] = None - # deprecated socketcan types + # Handle deprecated socketcan types if config['interface'] in ('socketcan_native', 'socketcan_ctypes'): - # Change this to a DeprecationWarning in future 2.x releases - # Remove completely in 3.0 - log.warning('%s is deprecated, use socketcan instead', config['interface']) + # DeprecationWarning in 3.x releases + # TODO: Remove completely in 4.0 + warnings.warn('{} is deprecated, use socketcan instead'.format(config['interface']), DeprecationWarning) config['interface'] = 'socketcan' if config['interface'] not in VALID_INTERFACES: @@ -190,10 +199,10 @@ def load_config(path=None, config=None): if 'bitrate' in config: config['bitrate'] = int(config['bitrate']) - can.log.debug("loaded can config: {}".format(config)) + can.log.debug("can config: {}".format(config)) return config - + def set_logging_level(level_name=None): """Set the logging level for the "can" logger. Expects one of: 'critical', 'error', 'warning', 'info', 'debug', 'subdebug' diff --git a/can/viewer.py b/can/viewer.py new file mode 100644 index 000000000..7b323e559 --- /dev/null +++ b/can/viewer.py @@ -0,0 +1,482 @@ +# coding: utf-8 +# +# Copyright (C) 2018 Kristian Sloth Lauszus. All rights reserved. +# +# Contact information +# ------------------- +# Kristian Sloth Lauszus +# Web : http://www.lauszus.com +# e-mail : lauszus@gmail.com + +from __future__ import absolute_import, print_function + +import argparse +import can +import curses +import os +import struct +import sys +import time + +from curses.ascii import ESC as KEY_ESC, SP as KEY_SPACE +from typing import Dict, List, Tuple, Union + +from can import __version__ + + +class CanViewer: + + def __init__(self, stdscr, bus, data_structs, testing=False): + self.stdscr = stdscr + self.bus = bus + self.data_structs = data_structs + + # Initialise the ID dictionary, start timestamp, scroll and variable for pausing the viewer + self.ids = {} + self.start_time = None + self.scroll = 0 + self.paused = False + + # Get the window dimensions - used for resizing the window + self.y, self.x = self.stdscr.getmaxyx() + + # Do not wait for key inputs, disable the cursor and choose the background color automatically + self.stdscr.nodelay(True) + curses.curs_set(0) + curses.use_default_colors() + + # Used to color error frames red + curses.init_pair(1, curses.COLOR_RED, -1) + + if not testing: # pragma: no cover + self.run() + + def run(self): + # Clear the terminal and draw the header + self.draw_header() + + while 1: + # Do not read the CAN-Bus when in paused mode + if not self.paused: + # Read the CAN-Bus and draw it in the terminal window + msg = self.bus.recv(timeout=1. / 1000.) + if msg is not None: + self.draw_can_bus_message(msg) + else: + # Sleep 1 ms, so the application does not use 100 % of the CPU resources + time.sleep(1. / 1000.) + + # Read the terminal input + key = self.stdscr.getch() + + # Stop program if the user presses ESC or 'q' + if key == KEY_ESC or key == ord('q'): + break + + # Clear by pressing 'c' + elif key == ord('c'): + self.ids = {} + self.start_time = None + self.scroll = 0 + self.draw_header() + + # Sort by pressing 's' + elif key == ord('s'): + # Sort frames based on the CAN-Bus ID + self.draw_header() + for i, key in enumerate(sorted(self.ids.keys())): + # Set the new row index, but skip the header + self.ids[key]['row'] = i + 1 + + # Do a recursive call, so the frames are repositioned + self.draw_can_bus_message(self.ids[key]['msg'], sorting=True) + + # Pause by pressing space + elif key == KEY_SPACE: + self.paused = not self.paused + + # Scroll by pressing up/down + elif key == curses.KEY_UP: + # Limit scrolling, so the user do not scroll passed the header + if self.scroll > 0: + self.scroll -= 1 + self.redraw_screen() + elif key == curses.KEY_DOWN: + # Limit scrolling, so the maximum scrolling position is one below the last line + if self.scroll <= len(self.ids) - self.y + 1: + self.scroll += 1 + self.redraw_screen() + + # Check if screen was resized + resized = curses.is_term_resized(self.y, self.x) + if resized is True: + self.y, self.x = self.stdscr.getmaxyx() + if hasattr(curses, 'resizeterm'): # pragma: no cover + curses.resizeterm(self.y, self.x) + self.redraw_screen() + + # Shutdown the CAN-Bus interface + self.bus.shutdown() + + # Unpack the data and then convert it into SI-units + @staticmethod + def unpack_data(cmd, cmd_to_struct, data): # type: (int, Dict, bytes) -> List[Union[float, int]] + if not cmd_to_struct or len(data) == 0: + # These messages do not contain a data package + return [] + + for key in cmd_to_struct.keys(): + if cmd == key if isinstance(key, int) else cmd in key: + value = cmd_to_struct[key] + if isinstance(value, tuple): + # The struct is given as the fist argument + struct_t = value[0] # type: struct.Struct + + # The conversion from raw values to SI-units are given in the rest of the tuple + values = [d // val if isinstance(val, int) else float(d) / val + for d, val in zip(struct_t.unpack(data), value[1:])] + else: + # No conversion from SI-units is needed + struct_t = value # type: struct.Struct + values = list(struct_t.unpack(data)) + + return values + else: + raise ValueError('Unknown command: 0x{:02X}'.format(cmd)) + + def draw_can_bus_message(self, msg, sorting=False): + # Use the CAN-Bus ID as the key in the dict + key = msg.arbitration_id + + # Sort the extended IDs at the bottom by setting the 32-bit high + if msg.is_extended_id: + key |= (1 << 32) + + new_id_added, length_changed = False, False + if not sorting: + # Check if it is a new message or if the length is not the same + if key not in self.ids: + new_id_added = True + # Set the start time when the first message has been received + if not self.start_time: + self.start_time = msg.timestamp + elif msg.dlc != self.ids[key]['msg'].dlc: + length_changed = True + + if new_id_added or length_changed: + # Increment the index if it was just added, but keep it if the length just changed + row = len(self.ids) + 1 if new_id_added else self.ids[key]['row'] + + # It's a new message ID or the length has changed, so add it to the dict + # The first index is the row index, the second is the frame counter, + # the third is a copy of the CAN-Bus frame + # and the forth index is the time since the previous message + self.ids[key] = {'row': row, 'count': 0, 'msg': msg, 'dt': 0} + else: + # Calculate the time since the last message and save the timestamp + self.ids[key]['dt'] = msg.timestamp - self.ids[key]['msg'].timestamp + + # Copy the CAN-Bus frame - this is used for sorting + self.ids[key]['msg'] = msg + + # Increment frame counter + self.ids[key]['count'] += 1 + + # Format the CAN-Bus ID as a hex value + arbitration_id_string = '0x{0:0{1}X}'.format(msg.arbitration_id, 8 if msg.is_extended_id else 3) + + # Generate data string + data_string = '' + if msg.dlc > 0: + data_string = ' '.join('{:02X}'.format(x) for x in msg.data) + + # Use red for error frames + if msg.is_error_frame: + color = curses.color_pair(1) + else: + color = curses.color_pair(0) + + # Now draw the CAN-Bus message on the terminal window + self.draw_line(self.ids[key]['row'], 0, str(self.ids[key]['count']), color) + self.draw_line(self.ids[key]['row'], 8, '{0:.6f}'.format(self.ids[key]['msg'].timestamp - self.start_time), + color) + self.draw_line(self.ids[key]['row'], 23, '{0:.6f}'.format(self.ids[key]['dt']), color) + self.draw_line(self.ids[key]['row'], 35, arbitration_id_string, color) + self.draw_line(self.ids[key]['row'], 47, str(msg.dlc), color) + self.draw_line(self.ids[key]['row'], 52, data_string, color) + + if self.data_structs: + try: + values_list = [] + for x in self.unpack_data(msg.arbitration_id, self.data_structs, msg.data): + if isinstance(x, float): + values_list.append('{0:.6f}'.format(x)) + else: + values_list.append(str(x)) + values_string = ' '.join(values_list) + self.draw_line(self.ids[key]['row'], 77, values_string, color) + except (ValueError, struct.error): + pass + + return self.ids[key] + + def draw_line(self, row, col, txt, *args): + if row - self.scroll < 0: + # Skip if we have scrolled passed the line + return + try: + self.stdscr.addstr(row - self.scroll, col, txt, *args) + except curses.error: + # Ignore if we are trying to write outside the window + # This happens if the terminal window is too small + pass + + def draw_header(self): + self.stdscr.erase() + self.draw_line(0, 0, 'Count', curses.A_BOLD) + self.draw_line(0, 8, 'Time', curses.A_BOLD) + self.draw_line(0, 23, 'dt', curses.A_BOLD) + self.draw_line(0, 35, 'ID', curses.A_BOLD) + self.draw_line(0, 47, 'DLC', curses.A_BOLD) + self.draw_line(0, 52, 'Data', curses.A_BOLD) + if self.data_structs: # Only draw if the dictionary is not empty + self.draw_line(0, 77, 'Parsed values', curses.A_BOLD) + + def redraw_screen(self): + # Trigger a complete redraw + self.draw_header() + for key in self.ids.keys(): + self.draw_can_bus_message(self.ids[key]['msg']) + + +# noinspection PyProtectedMember +class SmartFormatter(argparse.HelpFormatter): + + def _get_default_metavar_for_optional(self, action): + return action.dest.upper() + + def _format_usage(self, usage, actions, groups, prefix): + # Use uppercase for "Usage:" text + return super(SmartFormatter, self)._format_usage(usage, actions, groups, 'Usage: ') + + def _format_args(self, action, default_metavar): + if action.nargs != argparse.REMAINDER and action.nargs != argparse.ONE_OR_MORE: + return super(SmartFormatter, self)._format_args(action, default_metavar) + + # Use the metavar if "REMAINDER" or "ONE_OR_MORE" is set + get_metavar = self._metavar_formatter(action, default_metavar) + return '%s' % get_metavar(1) + + def _format_action_invocation(self, action): + if not action.option_strings or action.nargs == 0: + return super(SmartFormatter, self)._format_action_invocation(action) + + # Modified so "-s ARGS, --long ARGS" is replaced with "-s, --long ARGS" + else: + parts = [] + default = self._get_default_metavar_for_optional(action) + args_string = self._format_args(action, default) + for i, option_string in enumerate(action.option_strings): + if i == len(action.option_strings) - 1: + parts.append('%s %s' % (option_string, args_string)) + else: + parts.append('%s' % option_string) + return ', '.join(parts) + + def _split_lines(self, text, width): + # Allow to manually split the lines + if text.startswith('R|'): + return text[2:].splitlines() + return super(SmartFormatter, self)._split_lines(text, width) + + def _fill_text(self, text, width, indent): + if text.startswith('R|'): + # noinspection PyTypeChecker + return ''.join(indent + line + '\n' for line in text[2:].splitlines()) + else: + return super(SmartFormatter, self)._fill_text(text, width, indent) + + +def parse_args(args): + # Python versions >= 3.5 + kwargs = {} + if sys.version_info[0] * 10 + sys.version_info[1] >= 35: # pragma: no cover + kwargs = {'allow_abbrev': False} + + # Parse command line arguments + parser = argparse.ArgumentParser('python -m can.viewer', + description='A simple CAN viewer terminal application written in Python', + epilog='R|Shortcuts: ' + '\n +---------+-------------------------+' + '\n | Key | Description |' + '\n +---------+-------------------------+' + '\n | ESQ/q | Exit the viewer |' + '\n | c | Clear the stored frames |' + '\n | s | Sort the stored frames |' + '\n | SPACE | Pause the viewer |' + '\n | UP/DOWN | Scroll the viewer |' + '\n +---------+-------------------------+', + formatter_class=SmartFormatter, add_help=False, **kwargs) + + optional = parser.add_argument_group('Optional arguments') + + optional.add_argument('-h', '--help', action='help', help='Show this help message and exit') + + optional.add_argument('--version', action='version', help="Show program's version number and exit", + version='%(prog)s (version {version})'.format(version=__version__)) + + # Copied from: https://github.com/hardbyte/python-can/blob/develop/can/logger.py + optional.add_argument('-b', '--bitrate', type=int, help='''Bitrate to use for the given CAN interface''') + + optional.add_argument('-c', '--channel', help='''Most backend interfaces require some sort of channel. + For example with the serial interface the channel might be a rfcomm device: "/dev/rfcomm0" + with the socketcan interfaces valid channel examples include: "can0", "vcan0". + (default: use default for the specified interface)''') + + optional.add_argument('-d', '--decode', dest='decode', + help='R|Specify how to convert the raw bytes into real values.' + '\nThe ID of the frame is given as the first argument and the format as the second.' + '\nThe Python struct package is used to unpack the received data' + '\nwhere the format characters have the following meaning:' + '\n < = little-endian, > = big-endian' + '\n x = pad byte' + '\n c = char' + '\n ? = bool' + '\n b = int8_t, B = uint8_t' + '\n h = int16, H = uint16' + '\n l = int32_t, L = uint32_t' + '\n q = int64_t, Q = uint64_t' + '\n f = float (32-bits), d = double (64-bits)' + '\nFx to convert six bytes with ID 0x100 into uint8_t, uint16 and uint32_t:' + '\n $ python -m can.viewer -d "100:: (matches when & mask == can_id & mask)' + '\n ~ (matches when & mask != can_id & mask)' + '\nFx to show only frames with ID 0x100 to 0x103:' + '\n python -m can.viewer -f 100:7FC' + '\nNote that the ID and mask are alway interpreted as hex values', + metavar='{:,~}', nargs=argparse.ONE_OR_MORE, default='') + + optional.add_argument('-i', '--interface', dest='interface', + help='R|Specify the backend CAN interface to use.', + choices=sorted(can.VALID_INTERFACES)) + + # Print help message when no arguments are given + if len(args) == 0: + parser.print_help(sys.stderr) + import errno + raise SystemExit(errno.EINVAL) + + parsed_args = parser.parse_args(args) + + can_filters = [] + if len(parsed_args.filter) > 0: + # print('Adding filter/s', parsed_args.filter) + for flt in parsed_args.filter: + # print(filter) + if ':' in flt: + _ = flt.split(':') + can_id, can_mask = int(_[0], base=16), int(_[1], base=16) + elif '~' in flt: + can_id, can_mask = flt.split('~') + can_id = int(can_id, base=16) | 0x20000000 # CAN_INV_FILTER + can_mask = int(can_mask, base=16) & 0x20000000 # socket.CAN_ERR_FLAG + else: + raise argparse.ArgumentError(None, 'Invalid filter argument') + can_filters.append({'can_id': can_id, 'can_mask': can_mask}) + + # Dictionary used to convert between Python values and C structs represented as Python strings. + # If the value is 'None' then the message does not contain any data package. + # + # The struct package is used to unpack the received data. + # Note the data is assumed to be in little-endian byte order. + # < = little-endian, > = big-endian + # x = pad byte + # c = char + # ? = bool + # b = int8_t, B = uint8_t + # h = int16, H = uint16 + # l = int32_t, L = uint32_t + # q = int64_t, Q = uint64_t + # f = float (32-bits), d = double (64-bits) + # + # An optional conversion from real units to integers can be given as additional arguments. + # In order to convert from raw integer value the real units are multiplied with the values and similarly the values + # are divided by the value in order to convert from real units to raw integer values. + data_structs = {} # type: Dict[Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None]] + if len(parsed_args.decode) > 0: + if os.path.isfile(parsed_args.decode[0]): + with open(parsed_args.decode[0], 'r') as f: + structs = f.readlines() + else: + structs = parsed_args.decode + + for s in structs: + tmp = s.rstrip('\n').split(':') + + # The ID is given as a hex value, the format needs no conversion + key, fmt = int(tmp[0], base=16), tmp[1] + + # The scaling + scaling = [] # type: list + for t in tmp[2:]: + # First try to convert to int, if that fails, then convert to a float + try: + scaling.append(int(t)) + except ValueError: + scaling.append(float(t)) + + if scaling: + data_structs[key] = (struct.Struct(fmt),) + tuple(scaling) + else: + data_structs[key] = struct.Struct(fmt) + # print(data_structs[key]) + + return parsed_args, can_filters, data_structs + + +def main(): # pragma: no cover + parsed_args, can_filters, data_structs = parse_args(sys.argv[1:]) + + config = {'single_handle': True} + if can_filters: + config['can_filters'] = can_filters + if parsed_args.interface: + config['interface'] = parsed_args.interface + if parsed_args.bitrate: + config['bitrate'] = parsed_args.bitrate + + # Create a CAN-Bus interface + bus = can.Bus(parsed_args.channel, **config) + # print('Connected to {}: {}'.format(bus.__class__.__name__, bus.channel_info)) + + curses.wrapper(CanViewer, bus, data_structs) + + +if __name__ == '__main__': # pragma: no cover + # Catch ctrl+c + try: + main() + except KeyboardInterrupt: + pass diff --git a/doc/api.rst b/doc/api.rst index 1e40df2c9..8e657bd3c 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -15,6 +15,7 @@ A form of CAN interface is also required. bus message listeners + asyncio bcm diff --git a/doc/asyncio.rst b/doc/asyncio.rst new file mode 100644 index 000000000..f5bd7771b --- /dev/null +++ b/doc/asyncio.rst @@ -0,0 +1,24 @@ +.. _asyncio: + +Asyncio support +=============== + +The :mod:`asyncio` module built into Python 3.4 and later can be used to write +asynchronos code in a single thread. This library supports receiving messages +asynchronosly in an event loop using the :class:`can.Notifier` class. +There will still be one thread per CAN bus but the user application will execute +entirely in the event loop, allowing simpler concurrency without worrying about +threading issues. Interfaces that have a valid file descriptor will however be +supported natively without a thread. + +You can also use the :class:`can.AsyncBufferedReader` listener if you prefer +to write coroutine based code instead of using callbacks. + + +Example +------- + +Here is an example using both callback and coroutine based code: + +.. literalinclude:: ../examples/asyncio_demo.py + :language: python diff --git a/doc/bcm.rst b/doc/bcm.rst index f53bc1444..96e73d52d 100644 --- a/doc/bcm.rst +++ b/doc/bcm.rst @@ -1,57 +1,55 @@ +.. _bcm: + Broadcast Manager ================= -The broadcast manager isn't yet supported by all interfaces. -Currently SockerCAN and IXXAT are supported at least partially. -It allows the user to setup periodic message jobs. +.. module:: can.broadcastmanager -If periodic transmission is not supported natively, a software thread +The broadcast manager allows the user to setup periodic message jobs. +For example sending a particular message at a given period. The broadcast +manager supported natively by several interfaces and a software thread based scheduler is used as a fallback. -This example shows the socketcan_ctypes backend using the broadcast manager: - +This example shows the socketcan backend using the broadcast manager: .. literalinclude:: ../examples/cyclic.py :language: python :linenos: -Class based API ---------------- +Message Sending Tasks +~~~~~~~~~~~~~~~~~~~~~ + +The class based api for the broadcast manager uses a series of +`mixin classes `_. +All mixins inherit from :class:`~can.broadcastmanager.CyclicSendTaskABC` +which inherits from :class:`~can.broadcastmanager.CyclicTask`. .. autoclass:: can.broadcastmanager.CyclicTask :members: - .. autoclass:: can.broadcastmanager.CyclicSendTaskABC :members: -.. autoclass:: can.broadcastmanager.LimitedDurationCyclicSendTaskABC +.. autoclass:: LimitedDurationCyclicSendTaskABC :members: - -.. autoclass:: can.broadcastmanager.RestartableCyclicTaskABC +.. autoclass:: MultiRateCyclicSendTaskABC :members: - -.. autoclass:: can.broadcastmanager.ModifiableCyclicTaskABC +.. autoclass:: can.ModifiableCyclicTaskABC :members: -.. autoclass:: can.broadcastmanager.MultiRateCyclicSendTaskABC +.. autoclass:: can.RestartableCyclicTaskABC :members: -.. autoclass:: can.broadcastmanager.ThreadBasedCyclicSendTask - :members: - - Functional API -------------- -.. note:: - The functional API in :func:`can.broadcastmanager.send_periodic` is now deprecated. +.. warning:: + The functional API in :func:`can.broadcastmanager.send_periodic` is now deprecated + and will be removed in version 4.0. Use the object oriented API via :meth:`can.BusABC.send_periodic` instead. - .. autofunction:: can.broadcastmanager.send_periodic - diff --git a/doc/bin.rst b/doc/bin.rst deleted file mode 100644 index 685aed2b0..000000000 --- a/doc/bin.rst +++ /dev/null @@ -1,79 +0,0 @@ -Scripts -======= - -The following modules are callable from python-can. - -can.logger ----------- - -Command line help (``python -m can.logger --help``):: - - usage: python -m can.logger [-h] [-f LOG_FILE] [-v] [-c CHANNEL] - [-i {iscan,slcan,virtual,socketcan_ctypes,usb2can,ixxat,socketcan_native,kvaser,neovi,vector,nican,pcan,serial,remote,socketcan}] - [--filter ...] [-b BITRATE] - - Log CAN traffic, printing messages to stdout or to a given file - - optional arguments: - -h, --help show this help message and exit - -f LOG_FILE, --file_name LOG_FILE - Path and base log filename, extension can be .txt, - .asc, .csv, .db, .npz - -v How much information do you want to see at the command - line? You can add several of these e.g., -vv is DEBUG - -c CHANNEL, --channel CHANNEL - Most backend interfaces require some sort of channel. - For example with the serial interface the channel - might be a rfcomm device: "/dev/rfcomm0" With the - socketcan interfaces valid channel examples include: - "can0", "vcan0" - -i {iscan,slcan,virtual,socketcan_ctypes,usb2can,ixxat,socketcan_native,kvaser,neovi,vector,nican,pcan,serial,remote,socketcan}, --interface {iscan,slcan,virtual,socketcan_ctypes,usb2can,ixxat,socketcan_native,kvaser,neovi,vector,nican,pcan,serial,remote,socketcan} - Specify the backend CAN interface to use. If left - blank, fall back to reading from configuration files. - --filter ... Comma separated filters can be specified for the given - CAN interface: : (matches when - & mask == can_id & mask) - ~ (matches when & - mask != can_id & mask) - -b BITRATE, --bitrate BITRATE - Bitrate to use for the CAN bus. - - -can.player ----------- - -Command line help (``python -m can.player --help``):: - - usage: python -m can.player [-h] [-f LOG_FILE] [-v] [-c CHANNEL] - [-i {kvaser,virtual,slcan,nican,neovi,ixxat,serial,usb2can,socketcan_ctypes,remote,socketcan_native,iscan,vector,pcan,socketcan}] - [-b BITRATE] [--ignore-timestamps] [-g GAP] - [-s SKIP] - input-file - - Replay CAN traffic - - positional arguments: - input-file The file to replay. Supported types: .db, .blf - - optional arguments: - -h, --help show this help message and exit - -f LOG_FILE, --file_name LOG_FILE - Path and base log filename, extension can be .txt, - .asc, .csv, .db, .npz - -v Also print can frames to stdout. You can add several - of these to enable debugging - -c CHANNEL, --channel CHANNEL - Most backend interfaces require some sort of channel. - For example with the serial interface the channel - might be a rfcomm device: "/dev/rfcomm0" With the - socketcan interfaces valid channel examples include: - "can0", "vcan0" - -i {kvaser,virtual,slcan,nican,neovi,ixxat,serial,usb2can,socketcan_ctypes,remote,socketcan_native,iscan,vector,pcan,socketcan}, --interface {kvaser,virtual,slcan,nican,neovi,ixxat,serial,usb2can,socketcan_ctypes,remote,socketcan_native,iscan,vector,pcan,socketcan} - Specify the backend CAN interface to use. If left - blank, fall back to reading from configuration files. - -b BITRATE, --bitrate BITRATE - Bitrate to use for the CAN bus. - --ignore-timestamps Ignore timestamps (send all frames immediately with - minimum gap between frames) - -g GAP, --gap GAP minimum time between replayed frames - -s SKIP, --skip SKIP skip gaps greater than 's' seconds diff --git a/doc/bus.rst b/doc/bus.rst index 0a2291591..0b8814c8f 100644 --- a/doc/bus.rst +++ b/doc/bus.rst @@ -3,25 +3,40 @@ Bus --- -The :class:`can.BusABC` class, as the name suggests, provides an abstraction of a CAN bus. -The bus provides an abstract wrapper around a physical or virtual CAN Bus. +The :class:`~can.BusABC` class, as the name suggests, provides an abstraction of a CAN bus. +The bus provides a wrapper around a physical or virtual CAN Bus. +An interface specific instance of the :class:`~can.BusABC` is created by the :class:`~can.Bus` +class, for example:: + + vector_bus = can.Bus(interface='vector', ...) + +That bus is then able to handle the interface specific software/hardware interactions +and implements the :class:`~can.BusABC` API. A thread safe bus wrapper is also available, see `Thread safe bus`_. +Autoconfig Bus +'''''''''''''' + +.. autoclass:: can.Bus + :members: + :undoc-members: + API -'''' +''' .. autoclass:: can.BusABC :members: - :special-members: __iter__ + :undoc-members: Transmitting '''''''''''' -Writing to the bus is done by calling the :meth:`~can.BusABC.send` method and -passing a :class:`~can.Message` instance. +Writing individual messages to the bus is done by calling the :meth:`~can.BusABC.send` method +and passing a :class:`~can.Message` instance. Periodic sending is controlled by the +:ref:`broadcast manager `. Receiving @@ -53,18 +68,10 @@ Conflicting calls are executed by blocking until the bus is accessible. It can be used exactly like the normal :class:`~can.BusABC`: - # 'socketcan' is only an exemple interface, it works with all the others too + # 'socketcan' is only an example interface, it works with all the others too my_bus = can.ThreadSafeBus(interface='socketcan', channel='vcan0') my_bus.send(...) my_bus.recv(...) .. autoclass:: can.ThreadSafeBus :members: - -Autoconfig Bus --------------- - -.. autoclass:: can.interface.Bus - :members: - :special-members: __iter__ - diff --git a/doc/conf.py b/doc/conf.py index 9adaf9b7d..1c409a06a 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -28,6 +28,8 @@ # -- General configuration ----------------------------------------------------- +primary_domain = 'py' + # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', @@ -37,7 +39,9 @@ 'sphinx.ext.intersphinx', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', - 'sphinx.ext.graphviz'] + 'sphinx.ext.graphviz', + 'sphinxcontrib.programoutput' + ] # Now, you can use the alias name as a new role, e.g. :issue:`123`. extlinks = { @@ -45,7 +49,7 @@ } intersphinx_mapping = { - 'python': ('https://docs.python.org/3/', None) + 'python': ('https://docs.python.org/3/', None), } # If this is True, todo and todolist produce output, else they produce nothing. diff --git a/doc/configuration.rst b/doc/configuration.rst index 35eeab665..9297ab15c 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -36,16 +36,16 @@ Configuration File On Linux systems the config file is searched in the following paths: -1. ``~/can.conf`` -2. ``/etc/can.conf`` -3. ``$HOME/.can`` -4. ``$HOME/.canrc`` +#. ``~/can.conf`` +#. ``/etc/can.conf`` +#. ``$HOME/.can`` +#. ``$HOME/.canrc`` On Windows systems the config file is searched in the following paths: -1. ``~/can.conf`` -1. ``can.ini`` (current working directory) -2. ``$APPDATA/can.ini`` +#. ``~/can.conf`` +#. ``can.ini`` (current working directory) +#. ``$APPDATA/can.ini`` The configuration file sets the default interface and channel: @@ -57,6 +57,33 @@ The configuration file sets the default interface and channel: bitrate = +The configuration can also contain additional sections: + +:: + + [default] + interface = + channel = + bitrate = + + [HS] + # All the values from the 'default' section are inherited + channel = + bitrate = + + [MS] + # All the values from the 'default' section are inherited + channel = + bitrate = + + +:: + + from can.interfaces.interface import Bus + + hs_bus = Bus(config_section='HS') + ms_bus = Bus(config_section='MS') + Environment Variables --------------------- diff --git a/doc/development.rst b/doc/development.rst index 17e7f68ab..118c2a3cc 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -32,18 +32,20 @@ Creating a new interface/backend These steps are a guideline on how to add a new backend to python-can. -- Create a module (either a ``*.py`` or an entire subdirctory depending +- Create a module (either a ``*.py`` or an entire subdirectory depending on the complexity) inside ``can.interfaces`` - Implement the central part of the backend: the bus class that extends :class:`can.BusABC`. See below for more info on this one! - Register your backend bus class in ``can.interface.BACKENDS`` and - ``can.interfaces.VALID_INTERFACES``. -- Add docs where appropiate, like in ``doc/interfaces.rst`` and add - an entry in ``doc/interface/*``. -- Add tests in ``test/*`` where appropiate. + ``can.interfaces.VALID_INTERFACES`` in ``can.interfaces.__init__.py``. +- Add docs where appropriate. At a minimum add to ``doc/interfaces.rst`` and add + a new interface specific document in ``doc/interface/*``. +- Update ``doc/scripts.rst`` accordingly. +- Add tests in ``test/*`` where appropriate. + About the ``BusABC`` class -========================== +-------------------------- Concrete implementations *have to* implement the following: * :meth:`~can.BusABC.send` to send individual messages @@ -53,25 +55,25 @@ Concrete implementations *have to* implement the following: the underlying bus and/or channel They *might* implement the following: - * :meth:`~can.BusABC.flush_tx_buffer` to allow discrading any + * :meth:`~can.BusABC.flush_tx_buffer` to allow discarding any messages yet to be sent * :meth:`~can.BusABC.shutdown` to override how the bus should shut down - * :meth:`~can.BusABC.send_periodic` to override the software based - periodic sending and push it down to the kernel or hardware + * :meth:`~can.BusABC._send_periodic_internal` to override the software based + periodic sending and push it down to the kernel or hardware. * :meth:`~can.BusABC._apply_filters` to apply efficient filters - to lower level systems like the OS kernel or hardware + to lower level systems like the OS kernel or hardware. * :meth:`~can.BusABC._detect_available_configs` to allow the interface to report which configurations are currently available for new - connections + connections. * :meth:`~can.BusABC.state` property to allow reading and/or changing - the bus state + the bus state. .. note:: *TL;DR*: Only override :meth:`~can.BusABC._recv_internal`, never :meth:`~can.BusABC.recv` directly. - + Previously, concrete bus classes had to override :meth:`~can.BusABC.recv` directly instead of :meth:`~can.BusABC._recv_internal`, but that has changed to allow the abstract base class to handle in-software message @@ -79,6 +81,16 @@ They *might* implement the following: behaviour. Older (custom) interfaces might still be implemented like that and thus might not provide message filtering: +This is the entire ABC bus class with all internal methods: + +.. autoclass:: can.BusABC + :private-members: + :special-members: + :noindex: + + +Concrete instances are created by :class:`can.Bus`. + Code Structure -------------- @@ -108,14 +120,15 @@ Creating a new Release - Release from the ``master`` branch. - Update the library version in ``__init__.py`` using `semantic versioning `__. +- Check if any deprecations are pending. - Run all tests and examples against available hardware. - Update `CONTRIBUTORS.txt` with any new contributors. - For larger changes update ``doc/history.rst``. - Sanity check that documentation has stayed inline with code. -- Create a temporary virtual environment. Run ``python setup.py install`` and ``python setup.py test`` -- Create and upload the distribution: ``python setup.py sdist bdist_wheel`` -- Sign the packages with gpg ``gpg --detach-sign -a dist/python_can-X.Y.Z-py3-none-any.whl`` -- Upload with twine ``twine upload dist/python-can-X.Y.Z*`` -- In a new virtual env check that the package can be installed with pip: ``pip install python-can==X.Y.Z`` +- Create a temporary virtual environment. Run ``python setup.py install`` and ``python setup.py test``. +- Create and upload the distribution: ``python setup.py sdist bdist_wheel``. +- Sign the packages with gpg ``gpg --detach-sign -a dist/python_can-X.Y.Z-py3-none-any.whl``. +- Upload with twine ``twine upload dist/python-can-X.Y.Z*``. +- In a new virtual env check that the package can be installed with pip: ``pip install python-can==X.Y.Z``. - Create a new tag in the repository. - Check the release on PyPi, Read the Docs and GitHub. diff --git a/doc/doc-requirements.txt b/doc/doc-requirements.txt new file mode 100644 index 000000000..45026701b --- /dev/null +++ b/doc/doc-requirements.txt @@ -0,0 +1,2 @@ +sphinx>=1.8.1 +sphinxcontrib-programoutput diff --git a/doc/history.rst b/doc/history.rst index dfc7ad532..3ffc9bb3b 100644 --- a/doc/history.rst +++ b/doc/history.rst @@ -21,25 +21,39 @@ project in 2011. The socketcan interface was helped immensely by Phil Dixon who wrote a leaf-socketcan driver for Linux. The pcan interface was contributed by Albert Bloomfield in 2013. +Support for pcan on Mac was added by Kristian Sloth Lauszus in 2018. -The usb2can interface was contributed by Joshua Villyard in 2015 +The usb2can interface was contributed by Joshua Villyard in 2015. The IXXAT VCI interface was contributed by Giuseppe Corbelli and funded -by `Weightpack `__ in 2016 +by `Weightpack `__ in 2016. The NI-CAN and virtual interfaces plus the ASCII and BLF loggers were contributed by Christian Sandberg in 2016 and 2017. The BLF format is based on a C++ library by Toby Lorenz. -The slcan interface, ASCII listener and log logger and listener were contributed by Eduard Bröcker in 2017. +The slcan interface, ASCII listener and log logger and listener were contributed +by Eduard Bröcker in 2017. -The NeoVi interface for ICS (Intrepid Control Systems) devices was contributed +The NeoVi interface for ICS (Intrepid Control Systems) devices was contributed by Pierre-Luc Tessier Gagné in 2017. +Many improvements all over the library, cleanups, unifications as well as more +comprehensive documentation and CI testing was contributed by Felix Divo in 2017 +and 2018. + +The CAN viewer terminal script was contributed by Kristian Sloth Lauszus in 2018. Support for CAN within Python ----------------------------- -The 'socket' module contains support for SocketCAN from Python 3.3. +Python natively supports the CAN protocol from version 3.3 on, if running on Linux: -From Python 3.4 broadcast management commands are natively supported. +============== ============================================================== ==== +Python version Feature Link +============== ============================================================== ==== +3.3 Initial SocketCAN support `Docs `__ +3.4 Broadcast Banagement (BCM) commands are natively supported `Docs `__ +3.5 CAN FD support `Docs `__ +3.7 Support for CAN ISO-TP `Docs `__ +============== ============================================================== ==== diff --git a/doc/images/viewer.png b/doc/images/viewer.png new file mode 100644 index 000000000..fb91701b2 Binary files /dev/null and b/doc/images/viewer.png differ diff --git a/doc/index.rst b/doc/index.rst index 324f8cb48..f24831c7c 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -43,7 +43,7 @@ Contents: configuration api interfaces - bin + scripts development history diff --git a/doc/installation.rst b/doc/installation.rst index 0dc498583..add0f5cec 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -1,93 +1,93 @@ -Installation -============ - - -Install ``can`` with ``pip``: -:: - - $ pip install python-can - - -As most likely you will want to interface with some hardware, you may -also have to install platform dependencies. Be sure to check any other -specifics for your hardware in :doc:`interfaces`. - - -GNU/Linux dependencies ----------------------- - -Reasonably modern Linux Kernels (2.6.25 or newer) have an implementation -of ``socketcan``. This version of python-can will directly use socketcan -if called with Python 3.3 or greater, otherwise that interface is used -via ctypes. - -Windows dependencies --------------------- - -Kvaser -~~~~~~ - -To install ``python-can`` using the Kvaser CANLib SDK as the backend: - -1. Install the `latest stable release of - Python `__. - -2. Install `Kvaser's latest Windows CANLib - drivers `__. - -3. Test that Kvaser's own tools work to ensure the driver is properly - installed and that the hardware is working. - -PCAN -~~~~ - -Download and install the latest driver for your interface from -`PEAK-System's download page `__. - -Note that PCANBasic API timestamps count seconds from system startup. To -convert these to epoch times, the uptime library is used. If it is not -available, the times are returned as number of seconds from system -startup. To install the uptime library, run ``pip install uptime``. - -This library can take advantage of the `Python for Windows Extensions -`__ library if installed. -It will be used to get notified of new messages instead of -the CPU intensive polling that will otherwise have be used. - -IXXAT -~~~~~ - -To install ``python-can`` using the IXXAT VCI V3 SDK as the backend: - -1. Install `IXXAT's latest Windows VCI V3 SDK - drivers `__. - -2. Test that IXXAT's own tools (i.e. MiniMon) work to ensure the driver - is properly installed and that the hardware is working. - -NI-CAN -~~~~~~ - -Download and install the NI-CAN drivers from -`National Instruments `__. - -Currently the driver only supports 32-bit Python on Windows. - -neoVI -~~~~~ - -See :doc:`interfaces/neovi`. - - -Installing python-can in development mode ------------------------------------------ - -A "development" install of this package allows you to make changes locally -or pull updates from the Mercurial repository and use them without having to -reinstall. Download or clone the source repository then: - -:: - - python setup.py develop - - +Installation +============ + + +Install ``can`` with ``pip``: +:: + + $ pip install python-can + + +As most likely you will want to interface with some hardware, you may +also have to install platform dependencies. Be sure to check any other +specifics for your hardware in :doc:`interfaces`. + + +GNU/Linux dependencies +---------------------- + +Reasonably modern Linux Kernels (2.6.25 or newer) have an implementation +of ``socketcan``. This version of python-can will directly use socketcan +if called with Python 3.3 or greater, otherwise that interface is used +via ctypes. + +Windows dependencies +-------------------- + +Kvaser +~~~~~~ + +To install ``python-can`` using the Kvaser CANLib SDK as the backend: + +1. Install the `latest stable release of + Python `__. + +2. Install `Kvaser's latest Windows CANLib + drivers `__. + +3. Test that Kvaser's own tools work to ensure the driver is properly + installed and that the hardware is working. + +PCAN +~~~~ + +Download and install the latest driver for your interface from +`PEAK-System's download page `__. + +Note that PCANBasic API timestamps count seconds from system startup. To +convert these to epoch times, the uptime library is used. If it is not +available, the times are returned as number of seconds from system +startup. To install the uptime library, run ``pip install uptime``. + +This library can take advantage of the `Python for Windows Extensions +`__ library if installed. +It will be used to get notified of new messages instead of +the CPU intensive polling that will otherwise have be used. + +IXXAT +~~~~~ + +To install ``python-can`` using the IXXAT VCI V3 SDK as the backend: + +1. Install `IXXAT's latest Windows VCI V3 SDK + drivers `__. + +2. Test that IXXAT's own tools (i.e. MiniMon) work to ensure the driver + is properly installed and that the hardware is working. + +NI-CAN +~~~~~~ + +Download and install the NI-CAN drivers from +`National Instruments `__. + +Currently the driver only supports 32-bit Python on Windows. + +neoVI +~~~~~ + +See :doc:`interfaces/neovi`. + + +Installing python-can in development mode +----------------------------------------- + +A "development" install of this package allows you to make changes locally +or pull updates from the Mercurial repository and use them without having to +reinstall. Download or clone the source repository then: + +:: + + python setup.py develop + + diff --git a/doc/interfaces.rst b/doc/interfaces.rst index 00d1da37d..794959ee1 100644 --- a/doc/interfaces.rst +++ b/doc/interfaces.rst @@ -25,15 +25,15 @@ The available interfaces are: interfaces/virtual Additional interfaces can be added via a plugin interface. An external package -can register a new interface by using the ``python_can.interface`` entry point. +can register a new interface by using the ``can.interface`` entry point in its setup.py. The format of the entry point is ``interface_name=module:classname`` where -``classname`` is a :class:`can.BusABC` concrete implementation. +``classname`` is a concrete :class:`can.BusABC` implementation. :: entry_points={ - 'python_can.interface': [ + 'can.interface': [ "interface_name=module:classname", ] }, diff --git a/doc/interfaces/ixxat.rst b/doc/interfaces/ixxat.rst index f825908c4..ff52776b8 100644 --- a/doc/interfaces/ixxat.rst +++ b/doc/interfaces/ixxat.rst @@ -3,17 +3,14 @@ IXXAT Virtual CAN Interface =========================== - Interface to `IXXAT `__ Virtual CAN Interface V3 SDK. Works on Windows. +The Linux ECI SDK is currently unsupported, however on Linux some devices are +supported with :doc:`socketcan`. -.. note:: - - The Linux ECI SDK is currently unsupported, however on Linux some devices are supported with :doc:`socketcan`. - -The :meth:`~can.interfaces.ixxat.IXXATBus.send_periodic` method is supported +The :meth:`~can.interfaces.ixxat.canlib.IXXATBus.send_periodic` method is supported natively through the on-board cyclic transmit list. -Modifying cyclic messages is not possible. You will need to stop it, then +Modifying cyclic messages is not possible. You will need to stop it, and then start a new periodic message. @@ -21,9 +18,10 @@ Bus --- .. autoclass:: can.interfaces.ixxat.IXXATBus + :members: .. autoclass:: can.interfaces.ixxat.canlib.CyclicSendTask - + :members: Configuration file @@ -67,9 +65,3 @@ to receive (including RTR field). The can_id/mask must be specified according to IXXAT behaviour, that is bit 0 of can_id/mask parameters represents the RTR field in CAN frame. See IXXAT VCI documentation, section "Message filters" for more info. - -.. hint:: - - Module uses ``can.ixxat`` logger and at DEBUG level logs every frame - sent or received. It may be too verbose for your purposes. - diff --git a/doc/interfaces/pcan.rst b/doc/interfaces/pcan.rst index a0b156ba3..9bbaec9cb 100644 --- a/doc/interfaces/pcan.rst +++ b/doc/interfaces/pcan.rst @@ -3,27 +3,52 @@ PCAN Basic API ============== -.. warning:: +Interface to `Peak-System `__'s PCAN-Basic API. - This ``PCAN`` documentation is a work in progress. Feedback and revisions are most welcome! +Windows driver: https://www.peak-system.com/Downloads.76.0.html?&L=1 +Linux driver: https://www.peak-system.com/fileadmin/media/linux/index.htm#download and https://www.peak-system.com/Downloads.76.0.html?&L=1 (PCAN-Basic API (Linux)) -Interface to `Peak-System `__'s PCAN-Basic API. +Mac driver: http://www.mac-can.com Configuration ------------- -An example `can.ini` file for windows 7: +Here is an example configuration file for using `PCAN-USB `_: :: [default] interface = pcan channel = PCAN_USBBUS1 + state = can.bus.BusState.PASSIVE + bitrate = 500000 +``channel``: (default PCAN_USBBUS1) CAN interface name + +``state``: (default can.bus.BusState.ACTIVE) BusState of the channel + +``bitrate``: (default 500000) Channel bitrate + +Valid ``channel`` values: + +:: + + PCAN_ISABUSx + PCAN_DNGBUSx + PCAN_PCIBUSx + PCAN_USBBUSx + PCAN_PCCBUSx + PCAN_LANBUSx + +Where ``x`` should be replaced with the desired channel number starting at 1. + +Linux installation +------------------ + +Kernels >= 3.4 supports the PCAN adapters natively via :doc:`/interfaces/socketcan`, refer to: :ref:`socketcan-pcan`. Bus --- .. autoclass:: can.interfaces.pcan.PcanBus - diff --git a/doc/interfaces/serial.rst b/doc/interfaces/serial.rst index 651fb1b14..413d9cfd1 100644 --- a/doc/interfaces/serial.rst +++ b/doc/interfaces/serial.rst @@ -4,12 +4,16 @@ CAN over Serial =============== A text based interface. For example use over serial ports like ``/dev/ttyS1`` or ``/dev/ttyUSB0`` on Linux machines or ``COM1`` on Windows. +Remote ports can be also used via a special URL. Both raw TCP sockets as +also RFC2217 ports are supported: ``socket://192.168.254.254:5000`` or +``rfc2217://192.168.254.254:5000``. In addition a virtual loopback can be +used via ``loop://`` URL. The interface is a simple implementation that has been used for recording CAN traces. .. note:: - The properties extended_id, is_remote_frame and is_error_frame - from the class can.Message are not in use. These interface will not + The properties **extended_id**, **is_remote_frame** and **is_error_frame** + from the class:`~can.Message` are not in use. This interface will not send or receive flags for this properties. Bus @@ -95,4 +99,4 @@ Examples of serial frames | Start of frame | Timestamp | DLC | Arbitration ID | End of frame | +================+=====================+======+=====================+==============+ | 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x00 | 0xBBS | -+----------------+---------------------+------+---------------------+--------------+ \ No newline at end of file ++----------------+---------------------+------+---------------------+--------------+ diff --git a/doc/interfaces/slcan.rst b/doc/interfaces/slcan.rst index d47706d51..de182e8b8 100755 --- a/doc/interfaces/slcan.rst +++ b/doc/interfaces/slcan.rst @@ -5,19 +5,32 @@ CAN over Serial / SLCAN A text based interface: compatible to slcan-interfaces (slcan ASCII protocol) should also support LAWICEL direct. These interfaces can also be used with socketcan and slcand with Linux. -This driver directly uses the serial port, it makes slcan-compatible interfaces usable with Windows also. -Hint: Arduino-Interface could easyly be build https://github.com/latonita/arduino-canbus-monitor +This driver directly uses either the local or remote serial port, it makes slcan-compatible interfaces usable with Windows also. +Remote serial ports will be specified via special URL. Both raw TCP sockets as also RFC2217 ports are supported. -Usage: use ``port[@baurate]`` to open the device. -For example use ``/dev/ttyUSB0@115200`` or ``COM4@9600`` +Usage: use ``port or URL[@baurate]`` to open the device. +For example use ``/dev/ttyUSB0@115200`` or ``COM4@9600`` for local serial ports and +``socket://192.168.254.254:5000`` or ``rfc2217://192.168.254.254:5000`` for remote ports. + +.. note: + An Arduino-Interface could easily be build with this: + https://github.com/latonita/arduino-canbus-monitor + + +Supported devices +----------------- + +.. todo:: Document this. Bus --- .. autoclass:: can.interfaces.slcan.slcanBus + :members: + Internals --------- -.. TODO:: Document internals of slcan interface. +.. todo:: Document the internals of slcan interface. diff --git a/doc/interfaces/socketcan.rst b/doc/interfaces/socketcan.rst index 099c3e90e..7c4f89876 100644 --- a/doc/interfaces/socketcan.rst +++ b/doc/interfaces/socketcan.rst @@ -10,7 +10,7 @@ The full documentation for socketcan can be found in the kernel docs at Versions before 2.2 had two different implementations named ``socketcan_ctypes`` and ``socketcan_native``. These are now deprecated and the aliases to ``socketcan`` will be removed in - version 3.0. Future 2.x release may raise a DeprecationWarning. + version 4.0. 3.x releases raise a DeprecationWarning. Socketcan Quickstart @@ -48,6 +48,19 @@ existing ``can0`` interface with a bitrate of 1MB: sudo ip link set can0 up type can bitrate 1000000 +.. _socketcan-pcan: + +PCAN +~~~~ + +Kernels >= 3.4 supports the PCAN adapters natively via :doc:`/interfaces/socketcan`, so there is no need to install any drivers. The CAN interface can be brought like so: + +:: + + sudo modprobe peak_usb + sudo modprobe peak_pci + sudo ip link set can0 up type can bitrate 500000 + Send Test Message ^^^^^^^^^^^^^^^^^ @@ -205,7 +218,7 @@ Bus --- .. autoclass:: can.interfaces.socketcan.SocketcanBus - + .. method:: recv(timeout=None) Block waiting for a message from the Bus. diff --git a/doc/listeners.rst b/doc/listeners.rst index 3f2b57425..3434f2b3e 100644 --- a/doc/listeners.rst +++ b/doc/listeners.rst @@ -1,141 +1,162 @@ -Listeners -========= - -Listener --------- - -The Listener class is an "abstract" base class for any objects which wish to -register to receive notifications of new messages on the bus. A Listener can -be used in two ways; the default is to **call** the Listener with a new -message, or by calling the method **on_message_received**. - -Listeners are registered with :ref:`notifier` object(s) which ensure they are -notified whenever a new message is received. - -Subclasses of Listener that do not override **on_message_received** will cause -`NotImplementedError` to be thrown when a message is received on -the CAN bus. - -.. autoclass:: can.Listener - :members: - - -BufferedReader --------------- - -.. autoclass:: can.BufferedReader - :members: - - -Logger ------- - -The :class:`can.Logger` uses the following :class:`can.Listener` types to -create *.asc*, *.csv* and *.db* files with the messages received. - -.. autoclass:: can.Logger - :members: - - -Printer -------- - -.. autoclass:: can.Printer - :members: - - -CSVWriter ---------- - -.. autoclass:: can.CSVWriter - :members: - - -SqliteWriter ------------- - -.. autoclass:: can.SqliteWriter - :members: - -Database table format -~~~~~~~~~~~~~~~~~~~~~ - -The messages are written to the table ``messages`` in the sqlite database. -The table is created if it does not already exist. - -The entries are as follows: - -============== ============== ============== -Name Data type Note --------------- -------------- -------------- -ts REAL The timestamp of the message -arbitration_id INTEGER The arbitration id, might use the extended format -extended INTEGER ``1`` if the arbitration id uses the extended format, else ``0`` -remote INTEGER ``1`` if the message is a remote frame, else ``0`` -error INTEGER ``1`` if the message is an error frame, else ``0`` -dlc INTEGER The data length code (DLC) -data BLOB The content of the message -============== ============== ============== - - -ASC (.asc Logging format) -------------------------- -ASCWriter logs CAN data to an ASCII log file compatible with other CAN tools such as -Vector CANalyzer/CANoe and other. -Since no official specification exists for the format, it has been reverse- -engineered from existing log files. One description of the format can be found `here -`_. - - -.. note:: - - Channels will be converted to integers. - - -.. autoclass:: can.ASCWriter - :members: - -ASCReader reads CAN data from ASCII log files .asc -as further references can-utils can be used: -`asc2log `_, -`log2asc `_. - -.. autoclass:: can.ASCReader - :members: - - -Log (.log can-utils Logging format) ------------------------------------ - -CanutilsLogWriter logs CAN data to an ASCII log file compatible with `can-utils ` -As specification following references can-utils can be used: -`asc2log `_, -`log2asc `_. - - -.. autoclass:: can.io.CanutilsLogWriter - :members: - -CanutilsLogReader reads CAN data from ASCII log files .log - -.. autoclass:: can.io.CanutilsLogReader - :members: - - -BLF (Binary Logging Format) ---------------------------- - -Implements support for BLF (Binary Logging Format) which is a proprietary -CAN log format from Vector Informatik GmbH. - -The data is stored in a compressed format which makes it very compact. - -.. note:: - - Channels will be converted to integers. - -.. autoclass:: can.BLFWriter - :members: - -.. autoclass:: can.BLFReader - :members: +Listeners +========= + +Listener +-------- + +The Listener class is an "abstract" base class for any objects which wish to +register to receive notifications of new messages on the bus. A Listener can +be used in two ways; the default is to **call** the Listener with a new +message, or by calling the method **on_message_received**. + +Listeners are registered with :ref:`notifier` object(s) which ensure they are +notified whenever a new message is received. + +Subclasses of Listener that do not override **on_message_received** will cause +:class:`NotImplementedError` to be thrown when a message is received on +the CAN bus. + +.. autoclass:: can.Listener + :members: + +There are some listeners that already ship together with `python-can` +and are listed below. +Some of them allow messages to be written to files, and the corresponding file +readers are also documented here. + +.. warning :: + + Please note that writing and the reading a message might not always yield a + completely unchanged message again, since some properties are not (yet) + supported by some file formats. + + +BufferedReader +-------------- + +.. autoclass:: can.BufferedReader + :members: + +.. autoclass:: can.AsyncBufferedReader + :members: + + +Logger +------ + +The :class:`can.Logger` uses the following :class:`can.Listener` types to +create log files with different file types of the messages received. + +.. autoclass:: can.Logger + :members: + + +Printer +------- + +.. autoclass:: can.Printer + :members: + + +CSVWriter +--------- + +.. autoclass:: can.CSVWriter + :members: + +.. autoclass:: can.CSVReader + :members: + + +SqliteWriter +------------ + +.. autoclass:: can.SqliteWriter + :members: + +.. autoclass:: can.SqliteReader + :members: + + +Database table format +~~~~~~~~~~~~~~~~~~~~~ + +The messages are written to the table ``messages`` in the sqlite database +by default. The table is created if it does not already exist. + +The entries are as follows: + +============== ============== ============== +Name Data type Note +-------------- -------------- -------------- +ts REAL The timestamp of the message +arbitration_id INTEGER The arbitration id, might use the extended format +extended INTEGER ``1`` if the arbitration id uses the extended format, else ``0`` +remote INTEGER ``1`` if the message is a remote frame, else ``0`` +error INTEGER ``1`` if the message is an error frame, else ``0`` +dlc INTEGER The data length code (DLC) +data BLOB The content of the message +============== ============== ============== + + +ASC (.asc Logging format) +------------------------- +ASCWriter logs CAN data to an ASCII log file compatible with other CAN tools such as +Vector CANalyzer/CANoe and other. +Since no official specification exists for the format, it has been reverse- +engineered from existing log files. One description of the format can be found `here +`_. + + +.. note:: + + Channels will be converted to integers. + + +.. autoclass:: can.ASCWriter + :members: + +ASCReader reads CAN data from ASCII log files .asc, +as further references can-utils can be used: +`asc2log `_, +`log2asc `_. + +.. autoclass:: can.ASCReader + :members: + + +Log (.log can-utils Logging format) +----------------------------------- + +CanutilsLogWriter logs CAN data to an ASCII log file compatible with `can-utils ` +As specification following references can-utils can be used: +`asc2log `_, +`log2asc `_. + + +.. autoclass:: can.CanutilsLogWriter + :members: + +**CanutilsLogReader** reads CAN data from ASCII log files .log + +.. autoclass:: can.CanutilsLogReader + :members: + + +BLF (Binary Logging Format) +--------------------------- + +Implements support for BLF (Binary Logging Format) which is a proprietary +CAN log format from Vector Informatik GmbH. + +The data is stored in a compressed format which makes it very compact. + +.. note:: Channels will be converted to integers. + +.. autoclass:: can.BLFWriter + :members: + +The following class can be used to read messages from BLF file: + +.. autoclass:: can.BLFReader + :members: diff --git a/doc/message.rst b/doc/message.rst index cd350d9f1..fcb5e5e1a 100644 --- a/doc/message.rst +++ b/doc/message.rst @@ -23,6 +23,15 @@ Message 2.0B) in length, and ``python-can`` exposes this difference with the :attr:`~can.Message.is_extended_id` attribute. + .. attribute:: timestamp + + :type: float + + The timestamp field in a CAN message is a floating point number representing when + the message was received since the epoch in seconds. Where possible this will be + timestamped in hardware. + + .. attribute:: arbitration_id :type: int @@ -30,7 +39,7 @@ Message The frame identifier used for arbitration on the bus. The arbitration ID can take an int between 0 and the - maximum value allowed depending on the is_extended_id flag + maximum value allowed depending on the ``is_extended_id`` flag (either 2\ :sup:`11` - 1 for 11-bit IDs, or 2\ :sup:`29` - 1 for 29-bit identifiers). @@ -63,7 +72,7 @@ Message :type: int - The :abbr:`DLC (Data Link Count)` parameter of a CAN message is an integer + The :abbr:`DLC (Data Length Code)` parameter of a CAN message is an integer between 0 and 8 representing the frame payload length. In the case of a CAN FD message, this indicates the data length in @@ -82,12 +91,19 @@ Message represents the amount of data contained in the message, in remote frames it represents the amount of data being requested. + .. attribute:: channel + + :type: str or int or None + + This might store the channel from which the message came. + .. attribute:: is_extended_id :type: bool This flag controls the size of the :attr:`~can.Message.arbitration_id` field. + Previously this was exposed as `id_type`. >>> print(Message(extended_id=False)) Timestamp: 0.000000 ID: 0000 S DLC: 0 @@ -95,7 +111,10 @@ Message Timestamp: 0.000000 ID: 00000000 X DLC: 0 - Previously this was exposed as `id_type`. + .. note:: + + The :meth:`Message.__init__` argument ``extended_id`` has been deprecated in favor of + ``is_extended_id``, but will continue to work for the ``3.x`` release series. .. attribute:: is_error_frame @@ -141,15 +160,6 @@ Message If this is a CAN FD message, this indicates an error active state. - .. attribute:: timestamp - - :type: float - - The timestamp field in a CAN message is a floating point number representing when - the message was received since the epoch in seconds. Where possible this will be - timestamped in hardware. - - .. method:: __str__ A string representation of a CAN message: diff --git a/doc/scripts.rst b/doc/scripts.rst new file mode 100644 index 000000000..2b2290ad1 --- /dev/null +++ b/doc/scripts.rst @@ -0,0 +1,41 @@ +Scripts +======= + +The following modules are callable from python-can. + +They can be called for example by ``python -m can.logger`` or ``can_logger.py`` (if installed using pip). + +can.logger +---------- + +Command line help, called with ``--help``: + + +.. command-output:: python -m can.logger -h + + +can.player +---------- + +.. command-output:: python -m can.player -h + + +can.viewer +---------- + +A screenshot of the application can be seen below: + +.. image:: ../images/viewer.png + :width: 100% + +The first column is the number of times a frame with the particular ID that has been received, next is the timestamp of the frame relative to the first received message. The third column is the time between the current frame relative to the previous one. Next is the length of the frame, the data and then the decoded data converted according to the ``-d`` argument. The top red row indicates an error frame. + +Command line arguments +^^^^^^^^^^^^^^^^^^^^^^ + +By default the ``can.viewer`` uses the :doc:`/interfaces/socketcan` interface. All interfaces are supported and can be specified using the ``-i`` argument or configured following :doc:`/configuration`. + +The full usage page can be seen below: + +.. command-output:: python -m can.viewer -h + diff --git a/examples/asyncio_demo.py b/examples/asyncio_demo.py new file mode 100644 index 000000000..3e71ae6db --- /dev/null +++ b/examples/asyncio_demo.py @@ -0,0 +1,44 @@ +import asyncio +import can + +def print_message(msg): + """Regular callback function. Can also be a coroutine.""" + print(msg) + +async def main(): + can0 = can.Bus('vcan0', bustype='virtual', receive_own_messages=True) + reader = can.AsyncBufferedReader() + logger = can.Logger('logfile.asc') + + listeners = [ + print_message, # Callback function + reader, # AsyncBufferedReader() listener + logger # Regular Listener object + ] + # Create Notifier with an explicit loop to use for scheduling of callbacks + loop = asyncio.get_event_loop() + notifier = can.Notifier(can0, listeners, loop=loop) + # Start sending first message + can0.send(can.Message(arbitration_id=0)) + + print('Bouncing 10 messages...') + for _ in range(10): + # Wait for next message from AsyncBufferedReader + msg = await reader.get_message() + # Delay response + await asyncio.sleep(0.5) + msg.arbitration_id += 1 + can0.send(msg) + # Wait for last message to arrive + await reader.get_message() + print('Done!') + + # Clean-up + notifier.stop() + can0.shutdown() + +# Get the default event loop +loop = asyncio.get_event_loop() +# Run until main coroutine finishes +loop.run_until_complete(main()) +loop.close() diff --git a/examples/cyclic.py b/examples/cyclic.py index 281b7c43e..508440041 100755 --- a/examples/cyclic.py +++ b/examples/cyclic.py @@ -16,6 +16,7 @@ import time import can + logging.basicConfig(level=logging.INFO) @@ -36,14 +37,18 @@ def simple_periodic_send(bus): def limited_periodic_send(bus): print("Starting to send a message every 200ms for 1s") msg = can.Message(arbitration_id=0x12345678, data=[0, 0, 0, 0, 0, 0], extended_id=True) - task = bus.send_periodic(msg, 0.20, 1) + task = bus.send_periodic(msg, 0.20, 1, store_task=False) if not isinstance(task, can.LimitedDurationCyclicSendTaskABC): print("This interface doesn't seem to support a ") task.stop() return - time.sleep(1.5) - print("stopped cyclic send") + time.sleep(2) + print("Cyclic send should have stopped as duration expired") + # Note the (finished) task will still be tracked by the Bus + # unless we pass `store_task=False` to bus.send_periodic + # alternatively calling stop removes the task from the bus + #task.stop() def test_periodic_send_with_modifying_data(bus): @@ -104,15 +109,13 @@ def test_periodic_send_with_modifying_data(bus): reset_msg = can.Message(arbitration_id=0x00, data=[0, 0, 0, 0, 0, 0], extended_id=False) - for interface, channel in [ - ('socketcan_ctypes', 'can0'), - ('socketcan_native', 'can0') + ('socketcan', 'vcan0'), #('ixxat', 0) ]: print("Carrying out cyclic tests with {} interface".format(interface)) - bus = can.interface.Bus(bustype=interface, channel=channel, bitrate=500000) + bus = can.Bus(interface=interface, channel=channel, bitrate=500000) bus.send(reset_msg) simple_periodic_send(bus) diff --git a/examples/receive_all.py b/examples/receive_all.py old mode 100644 new mode 100755 index 6801f481d..90a4c68b6 --- a/examples/receive_all.py +++ b/examples/receive_all.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + from __future__ import print_function import can diff --git a/examples/serial_com.py b/examples/serial_com.py old mode 100644 new mode 100755 diff --git a/examples/vcan_filtered.py b/examples/vcan_filtered.py old mode 100644 new mode 100755 diff --git a/examples/virtual_can_demo.py b/examples/virtual_can_demo.py old mode 100644 new mode 100755 index 8845fc8d2..b69fb28da --- a/examples/virtual_can_demo.py +++ b/examples/virtual_can_demo.py @@ -2,30 +2,30 @@ # coding: utf-8 """ -This demo creates multiple processes of Producers to spam a socketcan bus. +This demo creates multiple processes of producers to spam a socketcan bus. """ -import time -import logging -import concurrent.futures +from time import sleep +from concurrent.futures import ProcessPoolExecutor import can -def producer(id): - """:param id: Spam the bus with messages including the data id.""" +def producer(id, message_count=16): + """Spam the bus with messages including the data id. - bus = can.interface.Bus(bustype='socketcan', channel='vcan0') - for i in range(16): - msg = can.Message(arbitration_id=0x0cf02200+id, data=[id, i, 0, 1, 3, 1, 4, 1]) - bus.send(msg) + :param int id: the id of the thread/process + """ + + with can.Bus(bustype='socketcan', channel='vcan0') as bus: + for i in range(message_count): + msg = can.Message(arbitration_id=0x0cf02200+id, data=[id, i, 0, 1, 3, 1, 4, 1]) + bus.send(msg) + sleep(1.0) + + print("Producer #{} finished sending {} messages".format(id, message_count)) - # TODO Issue #3: Need to keep running to ensure the writing threads stay alive. ? - time.sleep(2) if __name__ == "__main__": - #logging.getLogger('').setLevel(logging.DEBUG) - with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor: + with ProcessPoolExecutor(max_workers=4) as executor: executor.map(producer, range(5)) - -time.sleep(2) diff --git a/scripts/can_logger.py b/scripts/can_logger.py new file mode 100644 index 000000000..72a92b9d0 --- /dev/null +++ b/scripts/can_logger.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +See :mod:`can.logger`. +""" + +from __future__ import absolute_import + +from can.logger import main + + +if __name__ == "__main__": + main() diff --git a/scripts/can_player.py b/scripts/can_player.py new file mode 100644 index 000000000..afbd3df6e --- /dev/null +++ b/scripts/can_player.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +See :mod:`can.player`. +""" + +from __future__ import absolute_import + +from can.player import main + + +if __name__ == "__main__": + main() diff --git a/scripts/can_viewer.py b/scripts/can_viewer.py new file mode 100644 index 000000000..3c9ba738c --- /dev/null +++ b/scripts/can_viewer.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +See :mod:`can.viewer`. +""" + +from __future__ import absolute_import + +from can.viewer import main + + +if __name__ == "__main__": + main() diff --git a/setup.cfg b/setup.cfg index 2a9acf13d..21ffc0053 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,28 @@ [bdist_wheel] universal = 1 + +[metadata] +license_file = LICENSE.txt + +[tool:pytest] +addopts = -v --timeout=300 --cov=can --cov-config=setup.cfg + +[coverage:run] +# we could also use branch coverage +branch = False +# already specified by call to pytest using --cov=can +#source = can +omit = + # legacy code + can/CAN.py + +[coverage:report] +# two digits after decimal point +precision = 3 +show_missing = True +exclude_lines = + # Have to re-enable the standard pragma, see https://coverage.readthedocs.io/en/coverage-4.5.1a/config.html#syntax + pragma: no cover + + # Don't complain if non-runnable code isn't run: + if __name__ == .__main__.: diff --git a/setup.py b/setup.py index a0db6fa57..a40b01268 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,10 @@ python-can requires the setuptools package to be installed. """ -from sys import version_info +from __future__ import absolute_import + +from os import listdir +from os.path import isfile, join import re import logging from setuptools import setup, find_packages @@ -20,13 +23,24 @@ long_description = f.read() # Dependencies +extras_require = { + 'serial': ['pyserial~=3.0'], + 'neovi': ['python-ics>=2.12'] +} + tests_require = [ - 'mock ~= 2.0', - 'nose ~= 1.3.7', - 'pytest ~= 3.6', - 'pytest-timeout ~= 1.2', - 'pyserial ~= 3.0' -] + 'mock~=2.0', + 'nose~=1.3', + 'pytest~=3.6', + 'pytest-timeout~=1.2', + 'pytest-cov~=2.5', + 'codecov~=2.0', + 'future', + 'six' +] + extras_require['serial'] + +extras_require['test'] = tests_require + setup( # Description @@ -34,10 +48,39 @@ url="https://github.com/hardbyte/python-can", description="Controller Area Network interface module for Python", long_description=long_description, + classifiers=( + # a list of all available ones: https://pypi.org/classifiers/ + "Programming Language :: Python", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Information Technology", + "Intended Audience :: Manufacturing", + "Intended Audience :: Telecommunications Industry", + "Natural Language :: English", + "Topic :: System :: Logging", + "Topic :: System :: Monitoring", + "Topic :: System :: Networking", + "Topic :: System :: Hardware :: Hardware Drivers", + "Topic :: Utilities" + ), # Code version=version, packages=find_packages(exclude=["test", "test.*"]), + scripts=list(filter(isfile, (join("scripts/", f) for f in listdir("scripts/")))), # Author author="Brian Thorne", @@ -56,13 +99,11 @@ # see https://www.python.org/dev/peps/pep-0345/#version-specifiers python_requires=">=2.7,!=3.0,!=3.1,!=3.2,!=3.3", install_requires=[ - 'wrapt ~= 1.10', + 'wrapt~=1.10', + 'typing', + 'windows-curses;platform_system=="Windows"', ], - extras_require={ - 'serial': ['pyserial >= 3.0'], - 'neovi': ['python-ics >= 2.12'], - 'test': tests_require - }, + extras_require=extras_require, # Testing test_suite="nose.collector", diff --git a/test/back2back_test.py b/test/back2back_test.py index a93855dd2..25629bc0e 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -17,7 +17,6 @@ import can from .config import * -from .data.example_data import generate_message class Back2BackTestCase(unittest.TestCase): @@ -54,7 +53,7 @@ def _check_received_message(self, recv_msg, sent_msg): self.assertIsNotNone(recv_msg, "No message was received on %s" % self.INTERFACE_2) self.assertEqual(recv_msg.arbitration_id, sent_msg.arbitration_id) - self.assertEqual(recv_msg.id_type, sent_msg.id_type) + self.assertEqual(recv_msg.is_extended_id, sent_msg.is_extended_id) self.assertEqual(recv_msg.is_remote_frame, sent_msg.is_remote_frame) self.assertEqual(recv_msg.is_error_frame, sent_msg.is_error_frame) self.assertEqual(recv_msg.is_fd, sent_msg.is_fd) diff --git a/test/contextmanager_test.py b/test/contextmanager_test.py index a69dfd5e4..ea9321502 100644 --- a/test/contextmanager_test.py +++ b/test/contextmanager_test.py @@ -16,7 +16,7 @@ def setUp(self): self.msg_send = can.Message(extended_id=False, arbitration_id=0x100, data=data) def test_open_buses(self): - with can.interface.Bus(bustype='virtual') as bus_send, can.interface.Bus(bustype='virtual') as bus_recv: + with can.Bus(interface='virtual') as bus_send, can.Bus(interface='virtual') as bus_recv: bus_send.send(self.msg_send) msg_recv = bus_recv.recv() @@ -24,7 +24,7 @@ def test_open_buses(self): self.assertTrue(msg_recv) def test_use_closed_bus(self): - with can.interface.Bus(bustype='virtual') as bus_send, can.interface.Bus(bustype='virtual') as bus_recv: + with can.Bus(interface='virtual') as bus_send, can.Bus(interface='virtual') as bus_recv: bus_send.send(self.msg_send) # Receiving a frame after bus has been closed should raise a CanException diff --git a/test/data/example_data.py b/test/data/example_data.py index c3290683a..d4b1b877c 100644 --- a/test/data/example_data.py +++ b/test/data/example_data.py @@ -7,15 +7,28 @@ """ import random +from operator import attrgetter from can import Message # make tests more reproducible random.seed(13339115) + +def sort_messages(messages): + """ + Sorts the given messages by timestamps (ascending). + + :param Iterable[can.Message] messages: a sequence of messages to sort + :rtype: list + """ + return list(sorted(messages, key=attrgetter('timestamp'))) + + # some random number TEST_TIME = 1483389946.197 + # List of messages of different types that can be used in tests TEST_MESSAGES_BASE = [ Message( @@ -45,6 +58,22 @@ # empty data data=[0xFF, 0xFE, 0xFD], ), + Message( + # with channel as integer + channel=0, + ), + Message( + # with channel as integer + channel=42, + ), + Message( + # with channel as string + channel="vcan0", + ), + Message( + # with channel as string + channel="awesome_channel", + ), Message( arbitration_id=0xABCDEF, extended_id=True, timestamp=TEST_TIME, @@ -70,6 +99,8 @@ timestamp=TEST_TIME + 3.165 ), ] +TEST_MESSAGES_BASE = sort_messages(TEST_MESSAGES_BASE) + TEST_MESSAGES_REMOTE_FRAMES = [ Message( @@ -91,6 +122,8 @@ timestamp=TEST_TIME + 7858.67 ), ] +TEST_MESSAGES_REMOTE_FRAMES = sort_messages(TEST_MESSAGES_REMOTE_FRAMES) + TEST_MESSAGES_ERROR_FRAMES = [ Message( @@ -105,8 +138,12 @@ timestamp=TEST_TIME + 17.157 ) ] +TEST_MESSAGES_ERROR_FRAMES = sort_messages(TEST_MESSAGES_ERROR_FRAMES) + + +TEST_ALL_MESSAGES = sort_messages(TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + \ + TEST_MESSAGES_ERROR_FRAMES) -TEST_ALL_MESSAGES = TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + TEST_MESSAGES_ERROR_FRAMES TEST_COMMENTS = [ "This is the first comment", @@ -127,4 +164,4 @@ def generate_message(arbitration_id): and a non-extended ID. """ data = bytearray([random.randrange(0, 2 ** 8 - 1) for _ in range(8)]) - return Message(arbitration_id=arbitration_id, data=data, extended_id=False) + return Message(arbitration_id=arbitration_id, data=data, extended_id=False, timestamp=TEST_TIME) diff --git a/test/listener_test.py b/test/listener_test.py index b2a80382c..c25a6fb56 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -11,8 +11,9 @@ import random import logging import tempfile -import os.path import sqlite3 +import os +from os.path import join, dirname import can @@ -21,7 +22,7 @@ channel = 'virtual_channel_0' can.rc['interface'] = 'virtual' -logging.getLogger('').setLevel(logging.DEBUG) +logging.basicConfig(level=logging.DEBUG) # makes the random number generator deterministic random.seed(13339115) @@ -69,13 +70,13 @@ def tearDown(self): class ListenerTest(BusTest): def testBasicListenerCanBeAddedToNotifier(self): - a_listener = can.Listener() + a_listener = can.Printer() notifier = can.Notifier(self.bus, [a_listener], 0.1) notifier.stop() self.assertIn(a_listener, notifier.listeners) - + def testAddListenerToNotifier(self): - a_listener = can.Listener() + a_listener = can.Printer() notifier = can.Notifier(self.bus, [], 0.1) notifier.stop() self.assertNotIn(a_listener, notifier.listeners) @@ -83,7 +84,7 @@ def testAddListenerToNotifier(self): self.assertIn(a_listener, notifier.listeners) def testRemoveListenerFromNotifier(self): - a_listener = can.Listener() + a_listener = can.Printer() notifier = can.Notifier(self.bus, [a_listener], 0.1) notifier.stop() self.assertIn(a_listener, notifier.listeners) @@ -92,45 +93,65 @@ def testRemoveListenerFromNotifier(self): def testPlayerTypeResolution(self): def test_filetype_to_instance(extension, klass): - can_player = can.LogReader("test.{}".format(extension)) - self.assertIsInstance(can_player, klass) - if hasattr(can_player, "stop"): - can_player.stop() - - test_filetype_to_instance("asc", can.ASCReader) - test_filetype_to_instance("blf", can.BLFReader) - test_filetype_to_instance("csv", can.CSVReader) - test_filetype_to_instance("db" , can.SqliteReader) - test_filetype_to_instance("log", can.CanutilsLogReader) + print("testing: {}".format(extension)) + try: + if extension == ".blf": + delete = False + file_handler = open(join(dirname(__file__), "data/logfile.blf")) + else: + delete = True + file_handler = tempfile.NamedTemporaryFile(suffix=extension, delete=False) + + with file_handler as my_file: + filename = my_file.name + with can.LogReader(filename) as reader: + self.assertIsInstance(reader, klass) + finally: + if delete: + os.remove(filename) + + test_filetype_to_instance(".asc", can.ASCReader) + test_filetype_to_instance(".blf", can.BLFReader) + test_filetype_to_instance(".csv", can.CSVReader) + test_filetype_to_instance(".db" , can.SqliteReader) + test_filetype_to_instance(".log", can.CanutilsLogReader) # test file extensions that are not supported - with self.assertRaisesRegexp(NotImplementedError, "xyz_42"): - test_filetype_to_instance("xyz_42", can.Printer) - with self.assertRaises(Exception): - test_filetype_to_instance(None, can.Printer) + with self.assertRaisesRegexp(NotImplementedError, ".xyz_42"): + test_filetype_to_instance(".xyz_42", can.Printer) def testLoggerTypeResolution(self): def test_filetype_to_instance(extension, klass): - can_logger = can.Logger("test.{}".format(extension)) - self.assertIsInstance(can_logger, klass) - can_logger.stop() - - test_filetype_to_instance("asc", can.ASCWriter) - test_filetype_to_instance("blf", can.BLFWriter) - test_filetype_to_instance("csv", can.CSVWriter) - test_filetype_to_instance("db" , can.SqliteWriter) - test_filetype_to_instance("log", can.CanutilsLogWriter) - test_filetype_to_instance("txt", can.Printer) - - # test file extensions that should usa a fallback - test_filetype_to_instance(None, can.Printer) - test_filetype_to_instance("some_unknown_extention_42", can.Printer) + print("testing: {}".format(extension)) + try: + with tempfile.NamedTemporaryFile(suffix=extension, delete=False) as my_file: + filename = my_file.name + with can.Logger(filename) as writer: + self.assertIsInstance(writer, klass) + finally: + os.remove(filename) + + test_filetype_to_instance(".asc", can.ASCWriter) + test_filetype_to_instance(".blf", can.BLFWriter) + test_filetype_to_instance(".csv", can.CSVWriter) + test_filetype_to_instance(".db" , can.SqliteWriter) + test_filetype_to_instance(".log", can.CanutilsLogWriter) + test_filetype_to_instance(".txt", can.Printer) + + # test file extensions that should use a fallback + test_filetype_to_instance("", can.Printer) + test_filetype_to_instance(".", can.Printer) + test_filetype_to_instance(".some_unknown_extention_42", can.Printer) + with can.Logger(None) as logger: + self.assertIsInstance(logger, can.Printer) def testBufferedListenerReceives(self): a_listener = can.BufferedReader() a_listener(generate_message(0xDADADA)) - m = a_listener.get_message(0.1) - self.assertIsNotNone(m) + a_listener(generate_message(0xDADADA)) + self.assertIsNotNone(a_listener.get_message(0.1)) + a_listener.stop() + self.assertIsNotNone(a_listener.get_message(0.1)) if __name__ == '__main__': diff --git a/test/logformats_test.py b/test/logformats_test.py index e7d8b4bf7..039b3f768 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -10,17 +10,17 @@ different writer/reader pairs - e.g., some don't handle error frames and comments. +TODO: correctly set preserves_channel and adds_default_channel TODO: implement CAN FD support testing """ -from __future__ import print_function -from __future__ import absolute_import +from __future__ import print_function, absolute_import, division +import logging import unittest import tempfile -from time import sleep -import sqlite3 import os +from abc import abstractmethod, ABCMeta try: # Python 3 @@ -33,207 +33,424 @@ from .data.example_data import TEST_MESSAGES_BASE, TEST_MESSAGES_REMOTE_FRAMES, \ TEST_MESSAGES_ERROR_FRAMES, TEST_COMMENTS, \ - generate_message + sort_messages +from .message_helper import ComparingMessagesTestCase +logging.basicConfig(level=logging.DEBUG) -def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, sleep_time=None, - check_remote_frames=True, check_error_frames=True, - check_comments=False, round_timestamps=False): + +class ReaderWriterTest(unittest.TestCase, ComparingMessagesTestCase): """Tests a pair of writer and reader by writing all data first and then reading all data and checking if they could be reconstructed - correctly. - - :param unittest.TestCase test_case: the test case the use the assert methods on - :param Callable writer_constructor: the constructor of the writer class - :param Callable reader_constructor: the constructor of the reader class - - :param float sleep_time: specifies the time to sleep after writing all messages. - gets ignored when set to None - :param bool check_remote_frames: if True, also tests remote frames - :param bool check_error_frames: if True, also tests error frames - :param bool check_comments: if True, also inserts comments at some - locations and checks if they are contained anywhere literally - in the resulting file. The locations as selected randomly - but deterministically, which makes the test reproducible. - :param bool round_timestamps: if True, rounds timestamps using :meth:`~builtin.round` - before comparing the read messages/events + correctly. Optionally writes some comments as well. """ - assert isinstance(test_case, unittest.TestCase), \ - "test_case has to be a subclass of unittest.TestCase" - - if check_comments: - # we check this because of the lack of a common base class - # we filter for not starts with '__' so we do not get all the builtin - # methods when logging to the console - test_case.assertIn('log_event', [d for d in dir(writer_constructor) if not d.startswith('__')], - "cannot check comments with this writer: {}".format(writer_constructor)) - - # create a temporary file - temp = tempfile.NamedTemporaryFile('w', delete=False) - temp.close() - filename = temp.name - - # get all test messages - original_messages = TEST_MESSAGES_BASE - if check_remote_frames: - original_messages += TEST_MESSAGES_REMOTE_FRAMES - if check_error_frames: - original_messages += TEST_MESSAGES_ERROR_FRAMES - - # get all test comments - original_comments = TEST_COMMENTS - - # create writer - writer = writer_constructor(filename) - - # write - if check_comments: - # write messages and insert comments here and there + __test__ = False + + __metaclass__ = ABCMeta + + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + self._setup_instance() + + @abstractmethod + def _setup_instance(self): + """Hook for subclasses.""" + raise NotImplementedError() + + def _setup_instance_helper(self, + writer_constructor, reader_constructor, binary_file=False, + check_remote_frames=True, check_error_frames=True, check_fd=True, + check_comments=False, test_append=False, + allowed_timestamp_delta=0.0, + preserves_channel=True, adds_default_channel=None): + """ + :param Callable writer_constructor: the constructor of the writer class + :param Callable reader_constructor: the constructor of the reader class + :param bool binary_file: if True, opens files in binary and not in text mode + + :param bool check_remote_frames: if True, also tests remote frames + :param bool check_error_frames: if True, also tests error frames + :param bool check_fd: if True, also tests CAN FD frames + :param bool check_comments: if True, also inserts comments at some + locations and checks if they are contained anywhere literally + in the resulting file. The locations as selected randomly + but deterministically, which makes the test reproducible. + :param bool test_append: tests the writer in append mode as well + + :param float or int or None allowed_timestamp_delta: directly passed to :meth:`can.Message.equals` + :param bool preserves_channel: if True, checks that the channel attribute is preserved + :param any adds_default_channel: sets this as the channel when not other channel was given + ignored, if *preserves_channel* is True + """ + # get all test messages + self.original_messages = TEST_MESSAGES_BASE + if check_remote_frames: + self.original_messages += TEST_MESSAGES_REMOTE_FRAMES + if check_error_frames: + self.original_messages += TEST_MESSAGES_ERROR_FRAMES + if check_fd: + self.original_messages += [] # TODO: add TEST_MESSAGES_CAN_FD + + # sort them so that for example ASCWriter does not "fix" any messages with timestamp 0.0 + self.original_messages = sort_messages(self.original_messages) + + if check_comments: + # we check this because of the lack of a common base class + # we filter for not starts with '__' so we do not get all the builtin + # methods when logging to the console + attrs = [attr for attr in dir(writer_constructor) if not attr.startswith('__')] + assert 'log_event' in attrs, \ + "cannot check comments with this writer: {}".format(writer_constructor) + + # get all test comments + self.original_comments = TEST_COMMENTS if check_comments else () + + self.writer_constructor = writer_constructor + self.reader_constructor = reader_constructor + self.binary_file = binary_file + self.test_append_enabled = test_append + + ComparingMessagesTestCase.__init__(self, + allowed_timestamp_delta=allowed_timestamp_delta, + preserves_channel=preserves_channel) + #adds_default_channel=adds_default_channel # TODO inlcude in tests + + def setUp(self): + with tempfile.NamedTemporaryFile('w+', delete=False) as test_file: + self.test_file_name = test_file.name + + def tearDown(self): + os.remove(self.test_file_name) + del self.test_file_name + + def test_path_like_explicit_stop(self): + """testing with path-like and explicit stop() call""" + + # create writer + print("writing all messages/comments") + writer = self.writer_constructor(self.test_file_name) + self._write_all(writer) + self._ensure_fsync(writer) + writer.stop() + if hasattr(writer.file, 'closed'): + self.assertTrue(writer.file.closed) + + print("reading all messages") + reader = self.reader_constructor(self.test_file_name) + read_messages = list(reader) + # redundant, but this checks if stop() can be called multiple times + reader.stop() + if hasattr(writer.file, 'closed'): + self.assertTrue(writer.file.closed) + + # check if at least the number of messages matches + # could use assertCountEqual in later versions of Python and in the other methods + self.assertEqual(len(read_messages), len(self.original_messages), + "the number of written messages does not match the number of read messages") + + self.assertMessagesEqual(self.original_messages, read_messages) + self.assertIncludesComments(self.test_file_name) + + def test_path_like_context_manager(self): + """testing with path-like object and context manager""" + + # create writer + print("writing all messages/comments") + with self.writer_constructor(self.test_file_name) as writer: + self._write_all(writer) + self._ensure_fsync(writer) + w = writer + if hasattr(w.file, 'closed'): + self.assertTrue(w.file.closed) + + # read all written messages + print("reading all messages") + with self.reader_constructor(self.test_file_name) as reader: + read_messages = list(reader) + r = reader + if hasattr(r.file, 'closed'): + self.assertTrue(r.file.closed) + + # check if at least the number of messages matches; + self.assertEqual(len(read_messages), len(self.original_messages), + "the number of written messages does not match the number of read messages") + + self.assertMessagesEqual(self.original_messages, read_messages) + self.assertIncludesComments(self.test_file_name) + + def test_file_like_explicit_stop(self): + """testing with file-like object and explicit stop() call""" + + # create writer + print("writing all messages/comments") + my_file = open(self.test_file_name, 'wb' if self.binary_file else 'w') + writer = self.writer_constructor(my_file) + self._write_all(writer) + self._ensure_fsync(writer) + writer.stop() + if hasattr(my_file, 'closed'): + self.assertTrue(my_file.closed) + + print("reading all messages") + my_file = open(self.test_file_name, 'rb' if self.binary_file else 'r') + reader = self.reader_constructor(my_file) + read_messages = list(reader) + # redundant, but this checks if stop() can be called multiple times + reader.stop() + if hasattr(my_file, 'closed'): + self.assertTrue(my_file.closed) + + # check if at least the number of messages matches + # could use assertCountEqual in later versions of Python and in the other methods + self.assertEqual(len(read_messages), len(self.original_messages), + "the number of written messages does not match the number of read messages") + + self.assertMessagesEqual(self.original_messages, read_messages) + self.assertIncludesComments(self.test_file_name) + + def test_file_like_context_manager(self): + """testing with file-like object and context manager""" + + # create writer + print("writing all messages/comments") + my_file = open(self.test_file_name, 'wb' if self.binary_file else 'w') + with self.writer_constructor(my_file) as writer: + self._write_all(writer) + self._ensure_fsync(writer) + w = writer + if hasattr(my_file, 'closed'): + self.assertTrue(my_file.closed) + + # read all written messages + print("reading all messages") + my_file = open(self.test_file_name, 'rb' if self.binary_file else 'r') + with self.reader_constructor(my_file) as reader: + read_messages = list(reader) + r = reader + if hasattr(my_file, 'closed'): + self.assertTrue(my_file.closed) + + # check if at least the number of messages matches; + self.assertEqual(len(read_messages), len(self.original_messages), + "the number of written messages does not match the number of read messages") + + self.assertMessagesEqual(self.original_messages, read_messages) + self.assertIncludesComments(self.test_file_name) + + def test_append_mode(self): + """ + testing append mode with context manager and path-like object + """ + if not self.test_append_enabled: + raise unittest.SkipTest("do not test append mode") + + count = len(self.original_messages) + first_part = self.original_messages[:count // 2] + second_part = self.original_messages[count // 2:] + + # write first half + with self.writer_constructor(self.test_file_name) as writer: + for message in first_part: + writer(message) + self._ensure_fsync(writer) + + # use append mode for second half + try: + writer = self.writer_constructor(self.test_file_name, append=True) + except TypeError as e: + # maybe "append" is not a formal parameter (this is the case for SqliteWriter) + try: + writer = self.writer_constructor(self.test_file_name) + except TypeError: + # is the is still a problem, raise the initial error + raise e + with writer: + for message in second_part: + writer(message) + self._ensure_fsync(writer) + with self.reader_constructor(self.test_file_name) as reader: + read_messages = list(reader) + + self.assertMessagesEqual(self.original_messages, read_messages) + + def _write_all(self, writer): + """Writes messages and insert comments here and there.""" # Note: we make no assumptions about the length of original_messages and original_comments - for msg, comment in zip_longest(original_messages, original_comments, fillvalue=None): + for msg, comment in zip_longest(self.original_messages, self.original_comments, fillvalue=None): # msg and comment might be None if comment is not None: print("writing comment: ", comment) writer.log_event(comment) # we already know that this method exists - print("writing comment: ", comment) if msg is not None: print("writing message: ", msg) writer(msg) - print("writing message: ", msg) - else: - # ony write messages - for msg in original_messages: - print("writing message: ", msg) - writer(msg) - print("writing message: ", msg) - - # sleep and close the writer - if sleep_time is not None: - sleep(sleep_time) - writer.stop() + def _ensure_fsync(self, io_handler): + if hasattr(io_handler.file, 'fileno'): + io_handler.file.flush() + os.fsync(io_handler.file.fileno()) - # read all written messages - read_messages = list(reader_constructor(filename)) + def assertMessagesEqual(self, messages_1, messages_2): + """ + Checks the order and content of the individual messages. + """ + self.assertEqual(len(messages_1), len(messages_2)) - # check if at least the number of messages matches - test_case.assertEqual(len(read_messages), len(original_messages), - "the number of written messages does not match the number of read messages") + for message_1, message_2 in zip(messages_1, messages_2): + self.assertMessageEqual(message_1, message_2) - # check the order and content of the individual messages - for i, (read, original) in enumerate(zip(read_messages, original_messages)): - try: - # check everything except the timestamp - test_case.assertEqual(read, original) - # check the timestamp - if round_timestamps: - original.timestamp = round(original.timestamp) - read.timestamp = round(read.timestamp) - test_case.assertAlmostEqual(read.timestamp, original.timestamp, places=6) - except Exception as exception: - # attach the index - exception.args += ("test failed at index #{}".format(i), ) - raise exception - - # check if the comments are contained in the file - if check_comments: - # read the entire outout file - with open(filename, 'r') as file: - output_contents = file.read() - # check each, if they can be found in there literally - for comment in original_comments: - test_case.assertTrue(comment in output_contents) - - -class TestCanutilsLog(unittest.TestCase): - """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" - - def test_writer_and_reader(self): - _test_writer_and_reader(self, can.CanutilsLogWriter, can.CanutilsLogReader, - check_comments=False) - - -class TestAscFileFormat(unittest.TestCase): - """Tests can.ASCWriter and can.ASCReader""" + def assertIncludesComments(self, filename): + """ + Ensures that all comments are literally contained in the given file. - def test_writer_and_reader(self): - _test_writer_and_reader(self, can.ASCWriter, can.ASCReader, - check_comments=True, round_timestamps=True) + :param filename: the path-like object to use + """ + if self.original_comments: + # read the entire outout file + with open(filename, 'rb' if self.binary_file else 'r') as file: + output_contents = file.read() + # check each, if they can be found in there literally + for comment in self.original_comments: + self.assertIn(comment, output_contents) -class TestCsvFileFormat(unittest.TestCase): +class TestAscFileFormat(ReaderWriterTest): """Tests can.ASCWriter and can.ASCReader""" - def test_writer_and_reader(self): - _test_writer_and_reader(self, can.CSVWriter, can.CSVReader, - check_comments=False) + __test__ = True + def _setup_instance(self): + super(TestAscFileFormat, self)._setup_instance_helper( + can.ASCWriter, can.ASCReader, + check_fd=False, + check_comments=True, + preserves_channel=False, adds_default_channel=0 + ) -class TestSqliteDatabaseFormat(unittest.TestCase): - """Tests can.SqliteWriter and can.SqliteReader""" - - def test_writer_and_reader(self): - _test_writer_and_reader(self, can.SqliteWriter, can.SqliteReader, - sleep_time=can.SqliteWriter.MAX_TIME_BETWEEN_WRITES + 0.5, - check_comments=False) - - def testSQLWriterWritesToSameFile(self): - f = tempfile.NamedTemporaryFile('w', delete=False) - f.close() - first_listener = can.SqliteWriter(f.name) - first_listener(generate_message(0x01)) - - sleep(first_listener.MAX_TIME_BETWEEN_WRITES) - first_listener.stop() +class TestBlfFileFormat(ReaderWriterTest): + """Tests can.BLFWriter and can.BLFReader""" - second_listener = can.SqliteWriter(f.name) - second_listener(generate_message(0x02)) + __test__ = True - sleep(second_listener.MAX_TIME_BETWEEN_WRITES) + def _setup_instance(self): + super(TestBlfFileFormat, self)._setup_instance_helper( + can.BLFWriter, can.BLFReader, + binary_file=True, + check_fd=False, + check_comments=False, + allowed_timestamp_delta=1.0e-6, + preserves_channel=False, adds_default_channel=0 + ) - second_listener.stop() + def test_read_known_file(self): + logfile = os.path.join(os.path.dirname(__file__), "data", "logfile.blf") + with can.BLFReader(logfile) as reader: + messages = list(reader) + + expected = [ + can.Message( + timestamp=1.0, + extended_id=False, + arbitration_id=0x64, + data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]), + can.Message( + timestamp=73.0, + extended_id=True, + arbitration_id=0x1FFFFFFF, + is_error_frame=True,) + ] + + self.assertMessagesEqual(messages, expected) + + +class TestCanutilsFileFormat(ReaderWriterTest): + """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" - con = sqlite3.connect(f.name) + __test__ = True - with con: - c = con.cursor() + def _setup_instance(self): + super(TestCanutilsFileFormat, self)._setup_instance_helper( + can.CanutilsLogWriter, can.CanutilsLogReader, + check_fd=False, + test_append=True, check_comments=False, + preserves_channel=False, adds_default_channel='vcan0' + ) - c.execute("select COUNT() from messages") - self.assertEqual(2, c.fetchone()[0]) - c.execute("select * from messages") - msg1 = c.fetchone() - msg2 = c.fetchone() +class TestCsvFileFormat(ReaderWriterTest): + """Tests can.ASCWriter and can.ASCReader""" - self.assertEqual(msg1[1], 0x01) - self.assertEqual(msg2[1], 0x02) + __test__ = True + def _setup_instance(self): + super(TestCsvFileFormat, self)._setup_instance_helper( + can.CSVWriter, can.CSVReader, + check_fd=False, + test_append=True, check_comments=False, + preserves_channel=False, adds_default_channel=None + ) -class TestBlfFileFormat(unittest.TestCase): - """Tests can.BLFWriter and can.BLFReader""" - def test_writer_and_reader(self): - _test_writer_and_reader(self, can.BLFWriter, can.BLFReader, - check_comments=False) +class TestSqliteDatabaseFormat(ReaderWriterTest): + """Tests can.SqliteWriter and can.SqliteReader""" - def test_reader(self): - logfile = os.path.join(os.path.dirname(__file__), "data", "logfile.blf") - messages = list(can.BLFReader(logfile)) - self.assertEqual(len(messages), 2) - self.assertEqual(messages[0], - can.Message( - extended_id=False, - arbitration_id=0x64, - data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8])) - self.assertEqual(messages[0].channel, 0) - self.assertEqual(messages[1], - can.Message( - is_error_frame=True, - extended_id=True, - arbitration_id=0x1FFFFFFF)) - self.assertEqual(messages[1].channel, 0) + __test__ = True + + def _setup_instance(self): + super(TestSqliteDatabaseFormat, self)._setup_instance_helper( + can.SqliteWriter, can.SqliteReader, + check_fd=False, + test_append=True, check_comments=False, + preserves_channel=False, adds_default_channel=None + ) + + @unittest.skip("not implemented") + def test_file_like_explicit_stop(self): + pass + + @unittest.skip("not implemented") + def test_file_like_context_manager(self): + pass + + def test_read_all(self): + """ + testing :meth:`can.SqliteReader.read_all` with context manager and path-like object + """ + # create writer + print("writing all messages/comments") + with self.writer_constructor(self.test_file_name) as writer: + self._write_all(writer) + + # read all written messages + print("reading all messages") + with self.reader_constructor(self.test_file_name) as reader: + read_messages = list(reader.read_all()) + + # check if at least the number of messages matches; + self.assertEqual(len(read_messages), len(self.original_messages), + "the number of written messages does not match the number of read messages") + + self.assertMessagesEqual(self.original_messages, read_messages) + + +class TestPrinter(unittest.TestCase): + """Tests that can.Printer does not crash""" + + # TODO add CAN FD messages + messages = TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + TEST_MESSAGES_ERROR_FRAMES + + def test_not_crashes_with_stdout(self): + with can.Printer() as printer: + for message in self.messages: + printer(message) + + def test_not_crashes_with_file(self): + with tempfile.NamedTemporaryFile('w', delete=False) as temp_file: + with can.Printer(temp_file) as printer: + for message in self.messages: + printer(message) if __name__ == '__main__': diff --git a/test/message_helper.py b/test/message_helper.py new file mode 100644 index 000000000..497e5498f --- /dev/null +++ b/test/message_helper.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# coding: utf-8 + +from __future__ import absolute_import, print_function + +from copy import copy + + +class ComparingMessagesTestCase(object): + """Must be extended by a class also extending a unittest.TestCase. + """ + + def __init__(self, allowed_timestamp_delta=0.0, preserves_channel=True): + """ + :param float or int or None allowed_timestamp_delta: directly passed to :meth:`can.Message.equals` + :param bool preserves_channel: if True, checks that the channel attribute is preserved + """ + self.allowed_timestamp_delta = allowed_timestamp_delta + self.preserves_channel = preserves_channel + + def assertMessageEqual(self, message_1, message_2): + """ + Checks that two messages are equal, according to the given rules. + """ + + if message_1.equals(message_2, timestamp_delta=self.allowed_timestamp_delta): + return + elif self.preserves_channel: + print("Comparing: message 1: {!r}".format(message_1)) + print(" message 2: {!r}".format(message_2)) + self.fail("messages are unequal with allowed timestamp delta {}".format(self.allowed_timestamp_delta)) + else: + message_2 = copy(message_2) # make sure this method is pure + message_2.channel = message_1.channel + if message_1.equals(message_2, timestamp_delta=self.allowed_timestamp_delta): + return + else: + print("Comparing: message 1: {!r}".format(message_1)) + print(" message 2: {!r}".format(message_2)) + self.fail("messages are unequal with allowed timestamp delta {} even when ignoring channels" \ + .format(self.allowed_timestamp_delta)) diff --git a/test/network_test.py b/test/network_test.py index 830adceca..cf5acca76 100644 --- a/test/network_test.py +++ b/test/network_test.py @@ -101,7 +101,7 @@ def testProducerConsumer(self): self.assertIsNotNone(msg, "Didn't receive a message") #logging.debug("Received message {} with data: {}".format(i, msg.data)) - self.assertEqual(msg.id_type, self.extended_flags[i]) + self.assertEqual(msg.is_extended_id, self.extended_flags[i]) if not msg.is_remote_frame: self.assertEqual(msg.data, self.data[i]) self.assertEqual(msg.arbitration_id, self.ids[i]) diff --git a/test/notifier_test.py b/test/notifier_test.py index b11a97b56..ca462a2ad 100644 --- a/test/notifier_test.py +++ b/test/notifier_test.py @@ -2,6 +2,10 @@ # coding: utf-8 import unittest import time +try: + import asyncio +except ImportError: + asyncio = None import can @@ -16,6 +20,7 @@ def test_single_bus(self): bus.send(msg) self.assertIsNotNone(reader.get_message(1)) notifier.stop() + bus.shutdown() def test_multiple_bus(self): bus1 = can.Bus(0, bustype='virtual', receive_own_messages=True) @@ -33,6 +38,26 @@ def test_multiple_bus(self): self.assertIsNotNone(recv_msg) self.assertEqual(recv_msg.channel, 1) notifier.stop() + bus1.shutdown() + bus2.shutdown() + + +class AsyncNotifierTest(unittest.TestCase): + + @unittest.skipIf(asyncio is None, 'Test requires asyncio') + def test_asyncio_notifier(self): + loop = asyncio.get_event_loop() + bus = can.Bus('test', bustype='virtual', receive_own_messages=True) + reader = can.AsyncBufferedReader() + notifier = can.Notifier(bus, [reader], 0.1, loop=loop) + msg = can.Message() + bus.send(msg) + future = asyncio.wait_for(reader.get_message(), 1.0) + recv_msg = loop.run_until_complete(future) + self.assertIsNotNone(recv_msg) + notifier.stop() + bus.shutdown() + if __name__ == '__main__': diff --git a/test/serial_test.py b/test/serial_test.py index 1c189e4fb..5b26ae42a 100644 --- a/test/serial_test.py +++ b/test/serial_test.py @@ -7,14 +7,18 @@ Copyright: 2017 Boris Wenzlaff """ +from __future__ import division + import unittest from mock import patch import can from can.interfaces.serial.serial_can import SerialBus +from .message_helper import ComparingMessagesTestCase + -class SerialDummy: +class SerialDummy(object): """ Dummy to mock the serial communication """ @@ -36,20 +40,12 @@ def reset(self): self.msg = None -class SimpleSerialTest(unittest.TestCase): - MAX_TIMESTAMP = 0xFFFFFFFF / 1000 +class SimpleSerialTestBase(ComparingMessagesTestCase): - def setUp(self): - self.patcher = patch('serial.Serial') - self.mock_serial = self.patcher.start() - self.serial_dummy = SerialDummy() - self.mock_serial.return_value.write = self.serial_dummy.write - self.mock_serial.return_value.read = self.serial_dummy.read - self.addCleanup(self.patcher.stop) - self.bus = SerialBus('bus') + MAX_TIMESTAMP = 0xFFFFFFFF / 1000 - def tearDown(self): - self.serial_dummy.reset() + def __init__(self): + ComparingMessagesTestCase.__init__(self, allowed_timestamp_delta=None, preserves_channel=True) def test_rx_tx_min_max_data(self): """ @@ -59,7 +55,7 @@ def test_rx_tx_min_max_data(self): msg = can.Message(data=[b]) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) def test_rx_tx_min_max_dlc(self): """ @@ -71,7 +67,7 @@ def test_rx_tx_min_max_dlc(self): msg = can.Message(data=payload) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) def test_rx_tx_data_none(self): """ @@ -80,7 +76,7 @@ def test_rx_tx_data_none(self): msg = can.Message(data=None) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) def test_rx_tx_min_id(self): """ @@ -89,7 +85,7 @@ def test_rx_tx_min_id(self): msg = can.Message(arbitration_id=0) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) def test_rx_tx_max_id(self): """ @@ -98,7 +94,7 @@ def test_rx_tx_max_id(self): msg = can.Message(arbitration_id=536870911) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) def test_rx_tx_max_timestamp(self): """ @@ -108,7 +104,7 @@ def test_rx_tx_max_timestamp(self): msg = can.Message(timestamp=self.MAX_TIMESTAMP) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) self.assertEqual(msg.timestamp, msg_receive.timestamp) def test_rx_tx_max_timestamp_error(self): @@ -125,7 +121,7 @@ def test_rx_tx_min_timestamp(self): msg = can.Message(timestamp=0) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) self.assertEqual(msg.timestamp, msg_receive.timestamp) def test_rx_tx_min_timestamp_error(self): @@ -136,5 +132,37 @@ def test_rx_tx_min_timestamp_error(self): self.assertRaises(ValueError, self.bus.send, msg) +class SimpleSerialTest(unittest.TestCase, SimpleSerialTestBase): + + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + SimpleSerialTestBase.__init__(self) + + def setUp(self): + self.patcher = patch('serial.Serial') + self.mock_serial = self.patcher.start() + self.serial_dummy = SerialDummy() + self.mock_serial.return_value.write = self.serial_dummy.write + self.mock_serial.return_value.read = self.serial_dummy.read + self.addCleanup(self.patcher.stop) + self.bus = SerialBus('bus') + + def tearDown(self): + self.serial_dummy.reset() + + +class SimpleSerialLoopTest(unittest.TestCase, SimpleSerialTestBase): + + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + SimpleSerialTestBase.__init__(self) + + def setUp(self): + self.bus = SerialBus('loop://') + + def tearDown(self): + self.bus.shutdown() + + if __name__ == '__main__': unittest.main() diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 8763174a8..70a017937 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -13,8 +13,14 @@ import can from .config import * +from .message_helper import ComparingMessagesTestCase -class SimpleCyclicSendTaskTest(unittest.TestCase): + +class SimpleCyclicSendTaskTest(unittest.TestCase, ComparingMessagesTestCase): + + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + ComparingMessagesTestCase.__init__(self, allowed_timestamp_delta=None, preserves_channel=True) @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server") def test_cycle_time(self): @@ -30,10 +36,85 @@ def test_cycle_time(self): self.assertTrue(80 <= size <= 120, '100 +/- 20 messages should have been transmitted. But queue contained {}'.format(size)) last_msg = bus2.recv() - self.assertEqual(last_msg, msg) + self.assertMessageEqual(last_msg, msg) bus1.shutdown() bus2.shutdown() + + def test_removing_bus_tasks(self): + bus = can.interface.Bus(bustype='virtual') + tasks = [] + for task_i in range(10): + msg = can.Message(extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) + msg.arbitration_id = task_i + task = bus.send_periodic(msg, 0.1, 1) + tasks.append(task) + self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) + + assert len(bus._periodic_tasks) == 10 + + for task in tasks: + # Note calling task.stop will remove the task from the Bus's internal task management list + task.stop() + + assert len(bus._periodic_tasks) == 0 + bus.shutdown() + + def test_managed_tasks(self): + bus = can.interface.Bus(bustype='virtual', receive_own_messages=True) + tasks = [] + for task_i in range(3): + msg = can.Message(extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) + msg.arbitration_id = task_i + task = bus.send_periodic(msg, 0.1, 10, store_task=False) + tasks.append(task) + self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) + + assert len(bus._periodic_tasks) == 0 + + # Self managed tasks should still be sending messages + for _ in range(50): + received_msg = bus.recv(timeout=5.0) + assert received_msg is not None + assert received_msg.arbitration_id in {0, 1, 2} + + for task in tasks: + task.stop() + + for task in tasks: + assert task.thread.join(5.0) is None, "Task didn't stop before timeout" + + bus.shutdown() + + def test_stopping_perodic_tasks(self): + bus = can.interface.Bus(bustype='virtual') + tasks = [] + for task_i in range(10): + msg = can.Message(extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) + msg.arbitration_id = task_i + task = bus.send_periodic(msg, 0.1, 1) + tasks.append(task) + + assert len(bus._periodic_tasks) == 10 + # stop half the tasks using the task object + for task in tasks[::2]: + task.stop() + + assert len(bus._periodic_tasks) == 5 + + # stop the other half using the bus api + bus.stop_all_periodic_tasks(remove_tasks=False) + + for task in tasks: + assert task.thread.join(5.0) is None, "Task didn't stop before timeout" + + # Tasks stopped via `stop_all_periodic_tasks` with remove_tasks=False should + # still be associated with the bus (e.g. for restarting) + assert len(bus._periodic_tasks) == 5 + + bus.shutdown() + + if __name__ == '__main__': unittest.main() diff --git a/test/test_detect_available_configs.py b/test/test_detect_available_configs.py index c3e595891..153f91e3a 100644 --- a/test/test_detect_available_configs.py +++ b/test/test_detect_available_configs.py @@ -15,7 +15,7 @@ from can import detect_available_configs -from .config import IS_LINUX +from .config import IS_LINUX, IS_CI class TestDetectAvailableConfigs(unittest.TestCase): @@ -23,7 +23,7 @@ class TestDetectAvailableConfigs(unittest.TestCase): def test_count_returned(self): # At least virtual has to always return at least one interface self.assertGreaterEqual (len(detect_available_configs() ), 1) - self.assertEquals (len(detect_available_configs(interfaces=[]) ), 0) + self.assertEqual (len(detect_available_configs(interfaces=[]) ), 0) self.assertGreaterEqual (len(detect_available_configs(interfaces='virtual') ), 1) self.assertGreaterEqual (len(detect_available_configs(interfaces=['virtual']) ), 1) self.assertGreaterEqual (len(detect_available_configs(interfaces=None) ), 1) @@ -45,7 +45,7 @@ def test_content_socketcan(self): for config in configs: self.assertEqual(config['interface'], 'socketcan') - @unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") + @unittest.skipUnless(IS_LINUX and IS_CI, "socketcan is only available on Linux") def test_socketcan_on_ci_server(self): configs = detect_available_configs(interfaces='socketcan') self.assertGreaterEqual(len(configs), 1) diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 229381934..173b80d48 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -136,7 +136,7 @@ def test_recv_extended(self): msg = self.bus.recv() self.assertEqual(msg.arbitration_id, 0xc0ffef) self.assertEqual(msg.dlc, 8) - self.assertEqual(msg.id_type, True) + self.assertEqual(msg.is_extended_id, True) self.assertSequenceEqual(msg.data, self.msg_in_cue.data) self.assertTrue(now - 1 < msg.timestamp < now + 1) @@ -149,7 +149,7 @@ def test_recv_standard(self): msg = self.bus.recv() self.assertEqual(msg.arbitration_id, 0x123) self.assertEqual(msg.dlc, 2) - self.assertEqual(msg.id_type, False) + self.assertEqual(msg.is_extended_id, False) self.assertSequenceEqual(msg.data, [100, 101]) def test_available_configs(self): @@ -178,7 +178,7 @@ def canReadWait(self, handle, arb_id, data, dlc, flags, timestamp, timeout): dlc._obj.value = self.msg_in_cue.dlc data._obj.raw = self.msg_in_cue.data flags_temp = 0 - if self.msg_in_cue.id_type: + if self.msg_in_cue.is_extended_id: flags_temp |= constants.canMSG_EXT else: flags_temp |= constants.canMSG_STD diff --git a/test/test_load_file_config.py b/test/test_load_file_config.py new file mode 100644 index 000000000..52a45d734 --- /dev/null +++ b/test/test_load_file_config.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# coding: utf-8 +import shutil +import tempfile +import unittest +from tempfile import NamedTemporaryFile + +import can + + +class LoadFileConfigTest(unittest.TestCase): + configuration = { + 'default': {'interface': 'virtual', 'channel': '0'}, + 'one': {'interface': 'virtual', 'channel': '1'}, + 'two': {'channel': '2'}, + 'three': {'extra': 'extra value'}, + } + + def setUp(self): + # Create a temporary directory + self.test_dir = tempfile.mkdtemp() + + def tearDown(self): + # Remove the directory after the test + shutil.rmtree(self.test_dir) + + def _gen_configration_file(self, sections): + with NamedTemporaryFile(mode='w', dir=self.test_dir, + delete=False) as tmp_config_file: + content = [] + for section in sections: + content.append("[{}]".format(section)) + for k, v in self.configuration[section].items(): + content.append("{} = {}".format(k, v)) + tmp_config_file.write('\n'.join(content)) + return tmp_config_file.name + + def test_config_file_with_default(self): + tmp_config = self._gen_configration_file(['default']) + config = can.util.load_file_config(path=tmp_config) + self.assertEqual(config, self.configuration['default']) + + def test_config_file_with_default_and_section(self): + tmp_config = self._gen_configration_file(['default', 'one']) + + default = can.util.load_file_config(path=tmp_config) + self.assertEqual(default, self.configuration['default']) + + one = can.util.load_file_config(path=tmp_config, section='one') + self.assertEqual(one, self.configuration['one']) + + def test_config_file_with_section_only(self): + tmp_config = self._gen_configration_file(['one']) + config = can.util.load_file_config(path=tmp_config, section='one') + self.assertEqual(config, self.configuration['one']) + + def test_config_file_with_section_and_key_in_default(self): + expected = self.configuration['default'].copy() + expected.update(self.configuration['two']) + + tmp_config = self._gen_configration_file(['default', 'two']) + config = can.util.load_file_config(path=tmp_config, section='two') + self.assertEqual(config, expected) + + def test_config_file_with_section_missing_interface(self): + expected = self.configuration['two'].copy() + tmp_config = self._gen_configration_file(['two']) + config = can.util.load_file_config(path=tmp_config, section='two') + self.assertEqual(config, expected) + + def test_config_file_extra(self): + expected = self.configuration['default'].copy() + expected.update(self.configuration['three']) + + tmp_config = self._gen_configration_file(['default', 'three']) + config = can.util.load_file_config(path=tmp_config, section='three') + self.assertEqual(config, expected) + + def test_config_file_with_non_existing_section(self): + expected = {} + + tmp_config = self._gen_configration_file([ + 'default', 'one', 'two', 'three']) + config = can.util.load_file_config(path=tmp_config, section='zero') + self.assertEqual(config, expected) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_scripts.py b/test/test_scripts.py new file mode 100644 index 000000000..90687ccd7 --- /dev/null +++ b/test/test_scripts.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module tests that the scripts are all callable. +""" + +from __future__ import absolute_import + +import subprocess +import unittest +import sys +import errno +from abc import ABCMeta, abstractmethod + +from .config import * + +class CanScriptTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + # clean up the argument list so the call to the main() functions + # in test_does_not_crash() succeeds + sys.argv = sys.argv[:1] + + #: this is overridden by the subclasses + __test__ = False + + __metaclass__ = ABCMeta + + def test_do_commands_exist(self): + """This test calls each scripts once and veifies that the help + can be read without any other errors, like the script not being + found. + """ + for command in self._commands(): + try: + subprocess.check_output(command.split(), stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + return_code = e.returncode + output = e.output + else: + return_code = 0 + output = "-- NO OUTPUT --" + + allowed = [0, errno.EINVAL] + self.assertIn(return_code, allowed, + 'Calling "{}" failed (exit code was {} and not SUCCESS/0 or EINVAL/22):\n{}' + .format(command, return_code, output)) + + def test_does_not_crash(self): + # test import + module = self._import() + # test main method + with self.assertRaises(SystemExit) as cm: + module.main() + self.assertEqual(cm.exception.code, errno.EINVAL, + 'Calling main failed:\n{}'.format(command, e.output)) + + @abstractmethod + def _commands(self): + """Returns an Iterable of commands that should "succeed", meaning they exit + normally (exit code 0) or with the exit code for invalid arguments: EINVAL/22. + """ + pass + + @abstractmethod + def _import(self): + """Returns the modue of the script that has a main() function. + """ + pass + + +class TestLoggerScript(CanScriptTest): + + __test__ = True + + def _commands(self): + commands = [ + "python -m can.logger --help", + "python scripts/can_logger.py --help" + ] + if IS_UNIX: + commands += ["can_logger.py --help"] + return commands + + def _import(self): + import can.logger as module + return module + + +class TestPlayerScript(CanScriptTest): + + __test__ = True + + def _commands(self): + commands = [ + "python -m can.player --help", + "python scripts/can_player.py --help" + ] + if IS_UNIX: + commands += ["can_player.py --help"] + return commands + + def _import(self): + import can.player as module + return module + + +# TODO add #390 + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_viewer.py b/test/test_viewer.py new file mode 100644 index 000000000..47e5ef7e7 --- /dev/null +++ b/test/test_viewer.py @@ -0,0 +1,452 @@ +#!/usr/bin/python +# coding: utf-8 +# +# Copyright (C) 2018 Kristian Sloth Lauszus. All rights reserved. +# +# Contact information +# ------------------- +# Kristian Sloth Lauszus +# Web : http://www.lauszus.com +# e-mail : lauszus@gmail.com + +from __future__ import absolute_import + +import argparse +import can +import curses +import math +import pytest +import random +import struct +import time +import unittest +import os +import six + +from typing import Dict, Tuple, Union + +try: + # noinspection PyCompatibility + from unittest.mock import Mock, patch +except ImportError: + # noinspection PyPackageRequirements + from mock import Mock, patch + +from can.viewer import KEY_ESC, KEY_SPACE, CanViewer, parse_args + + +# noinspection SpellCheckingInspection,PyUnusedLocal +class StdscrDummy: + + def __init__(self): + self.key_counter = 0 + + @staticmethod + def clear(): + pass + + @staticmethod + def erase(): + pass + + @staticmethod + def getmaxyx(): + # Set y-value, so scrolling gets tested + return 1, 1 + + @staticmethod + def addstr(row, col, txt, *args): + assert row >= 0 + assert col >= 0 + assert txt is not None + # Raise an exception 50 % of the time, so we can make sure the code handles it + if random.random() < .5: + raise curses.error + + @staticmethod + def nodelay(_bool): + pass + + def getch(self): + self.key_counter += 1 + if self.key_counter == 1: + # Send invalid key + return -1 + elif self.key_counter == 2: + return ord('c') # Clear + elif self.key_counter == 3: + return KEY_SPACE # Pause + elif self.key_counter == 4: + return KEY_SPACE # Unpause + elif self.key_counter == 5: + return ord('s') # Sort + + # Keep scrolling until it exceeds the number of messages + elif self.key_counter <= 100: + return curses.KEY_DOWN + # Scroll until the header is back as the first line and then scroll over the limit + elif self.key_counter <= 200: + return curses.KEY_UP + + return KEY_ESC + + +class CanViewerTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + # Set seed, so the tests are not affected + random.seed(0) + + def setUp(self): + stdscr = StdscrDummy() + config = {'interface': 'virtual', 'receive_own_messages': True} + bus = can.Bus(**config) + data_structs = None + + patch_curs_set = patch('curses.curs_set') + patch_curs_set.start() + self.addCleanup(patch_curs_set.stop) + + patch_use_default_colors = patch('curses.use_default_colors') + patch_use_default_colors.start() + self.addCleanup(patch_use_default_colors.stop) + + patch_init_pair = patch('curses.init_pair') + patch_init_pair.start() + self.addCleanup(patch_init_pair.stop) + + patch_color_pair = patch('curses.color_pair') + patch_color_pair.start() + self.addCleanup(patch_color_pair.stop) + + patch_is_term_resized = patch('curses.is_term_resized') + mock_is_term_resized = patch_is_term_resized.start() + mock_is_term_resized.return_value = True if random.random() < .5 else False + self.addCleanup(patch_is_term_resized.stop) + + if hasattr(curses, 'resizeterm'): + patch_resizeterm = patch('curses.resizeterm') + patch_resizeterm.start() + self.addCleanup(patch_resizeterm.stop) + + self.can_viewer = CanViewer(stdscr, bus, data_structs, testing=True) + + def tearDown(self): + # Run the viewer after the test, this is done, so we can receive the CAN-Bus messages and make sure that they + # are parsed correctly + self.can_viewer.run() + + def test_send(self): + # CANopen EMCY + data = [1, 2, 3, 4, 5, 6, 7] # Wrong length + msg = can.Message(arbitration_id=0x080 + 1, data=data, extended_id=False) + self.can_viewer.bus.send(msg) + + data = [1, 2, 3, 4, 5, 6, 7, 8] + msg = can.Message(arbitration_id=0x080 + 1, data=data, extended_id=False) + self.can_viewer.bus.send(msg) + + # CANopen HEARTBEAT + data = [0x05] # Operational + msg = can.Message(arbitration_id=0x700 + 0x7F, data=data, extended_id=False) + self.can_viewer.bus.send(msg) + + # Send non-CANopen message + data = [1, 2, 3, 4, 5, 6, 7, 8] + msg = can.Message(arbitration_id=0x101, data=data, extended_id=False) + self.can_viewer.bus.send(msg) + + # Send the same command, but with another data length + data = [1, 2, 3, 4, 5, 6] + msg = can.Message(arbitration_id=0x101, data=data, extended_id=False) + self.can_viewer.bus.send(msg) + + # Message with extended id + data = [1, 2, 3, 4, 5, 6, 7, 8] + msg = can.Message(arbitration_id=0x123456, data=data, extended_id=True) + self.can_viewer.bus.send(msg) + # self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None)) + + # Send the same message again to make sure that resending works and dt is correct + time.sleep(.1) + self.can_viewer.bus.send(msg) + + # Send error message + msg = can.Message(is_error_frame=True) + self.can_viewer.bus.send(msg) + + def test_receive(self): + # Send the messages again, but this time the test code will receive it + self.test_send() + + data_structs = { + # For converting the EMCY and HEARTBEAT messages + 0x080 + 0x01: struct.Struct('ff'), + } + # Receive the messages we just sent in 'test_canopen' + while 1: + msg = self.can_viewer.bus.recv(timeout=0) + if msg is not None: + self.can_viewer.data_structs = data_structs if msg.arbitration_id != 0x101 else None + _id = self.can_viewer.draw_can_bus_message(msg) + if _id['msg'].arbitration_id == 0x101: + # Check if the counter is reset when the length has changed + self.assertEqual(_id['count'], 1) + elif _id['msg'].arbitration_id == 0x123456: + # Check if the counter is incremented + if _id['dt'] == 0: + self.assertEqual(_id['count'], 1) + else: + self.assertTrue(pytest.approx(_id['dt'], 0.1)) # dt should be ~0.1 s + self.assertEqual(_id['count'], 2) + else: + # Make sure dt is 0 + if _id['count'] == 1: + self.assertEqual(_id['dt'], 0) + else: + break + + # Convert it into raw integer values and then pack the data + @staticmethod + def pack_data(cmd, cmd_to_struct, *args): # type: (int, Dict, Union[*float, *int]) -> bytes + if not cmd_to_struct or len(args) == 0: + # If no arguments are given, then the message does not contain a data package + return b'' + + for key in cmd_to_struct.keys(): + if cmd == key if isinstance(key, int) else cmd in key: + value = cmd_to_struct[key] + if isinstance(value, tuple): + # The struct is given as the fist argument + struct_t = value[0] # type: struct.Struct + + # The conversion from SI-units to raw values are given in the rest of the tuple + fmt = struct_t.format + if isinstance(fmt, six.string_types): # pragma: no cover + # Needed for Python 3.7 + fmt = six.b(fmt) + + # Make sure the endian is given as the first argument + assert six.byte2int(fmt) == ord('<') or six.byte2int(fmt) == ord('>') + + # Disable rounding if the format is a float + data = [] + for c, arg, val in zip(six.iterbytes(fmt[1:]), args, value[1:]): + if c == ord('f'): + data.append(arg * val) + else: + data.append(round(arg * val)) + else: + # No conversion from SI-units is needed + struct_t = value # type: struct.Struct + data = args + + return struct_t.pack(*data) + else: + raise ValueError('Unknown command: 0x{:02X}'.format(cmd)) + + def test_pack_unpack(self): + CANOPEN_TPDO1 = 0x180 + CANOPEN_TPDO2 = 0x280 + CANOPEN_TPDO3 = 0x380 + CANOPEN_TPDO4 = 0x480 + + # Dictionary used to convert between Python values and C structs represented as Python strings. + # If the value is 'None' then the message does not contain any data package. + # + # The struct package is used to unpack the received data. + # Note the data is assumed to be in little-endian byte order. + # < = little-endian, > = big-endian + # x = pad byte + # c = char + # ? = bool + # b = int8_t, B = uint8_t + # h = int16, H = uint16 + # l = int32_t, L = uint32_t + # q = int64_t, Q = uint64_t + # f = float (32-bits), d = double (64-bits) + # + # An optional conversion from real units to integers can be given as additional arguments. + # In order to convert from raw integer value the SI-units are multiplied with the values and similarly the values + # are divided by the value in order to convert from real units to raw integer values. + data_structs = { + # CANopen node 1 + CANOPEN_TPDO1 + 1: struct.Struct('lL'), + (CANOPEN_TPDO3 + 2, CANOPEN_TPDO4 + 2): struct.Struct('>LL'), + } # type: Dict[Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None]] + + raw_data = self.pack_data(CANOPEN_TPDO1 + 1, data_structs, -7, 13, -1024, 2048, 0xFFFF) + parsed_data = CanViewer.unpack_data(CANOPEN_TPDO1 + 1, data_structs, raw_data) + self.assertListEqual(parsed_data, [-7, 13, -1024, 2048, 0xFFFF]) + self.assertTrue(all(isinstance(d, int) for d in parsed_data)) + + raw_data = self.pack_data(CANOPEN_TPDO2 + 1, data_structs, 12.34, 4.5, 6) + parsed_data = CanViewer.unpack_data(CANOPEN_TPDO2 + 1, data_structs, raw_data) + self.assertTrue(pytest.approx(parsed_data, [12.34, 4.5, 6])) + self.assertTrue(isinstance(parsed_data[0], float) and isinstance(parsed_data[1], float) and + isinstance(parsed_data[2], int)) + + raw_data = self.pack_data(CANOPEN_TPDO3 + 1, data_structs, 123.45, 67.89) + parsed_data = CanViewer.unpack_data(CANOPEN_TPDO3 + 1, data_structs, raw_data) + self.assertTrue(pytest.approx(parsed_data, [123.45, 67.89])) + self.assertTrue(all(isinstance(d, float) for d in parsed_data)) + + raw_data = self.pack_data(CANOPEN_TPDO4 + 1, data_structs, math.pi / 2., math.pi) + parsed_data = CanViewer.unpack_data(CANOPEN_TPDO4 + 1, data_structs, raw_data) + self.assertTrue(pytest.approx(parsed_data, [math.pi / 2., math.pi])) + self.assertTrue(all(isinstance(d, float) for d in parsed_data)) + + raw_data = self.pack_data(CANOPEN_TPDO1 + 2, data_structs) + parsed_data = CanViewer.unpack_data(CANOPEN_TPDO1 + 2, data_structs, raw_data) + self.assertListEqual(parsed_data, []) + self.assertIsInstance(parsed_data, list) + + raw_data = self.pack_data(CANOPEN_TPDO2 + 2, data_structs, -2147483648, 0xFFFFFFFF) + parsed_data = CanViewer.unpack_data(CANOPEN_TPDO2 + 2, data_structs, raw_data) + self.assertListEqual(parsed_data, [-2147483648, 0xFFFFFFFF]) + + raw_data = self.pack_data(CANOPEN_TPDO3 + 2, data_structs, 0xFF, 0xFFFF) + parsed_data = CanViewer.unpack_data(CANOPEN_TPDO3 + 2, data_structs, raw_data) + self.assertListEqual(parsed_data, [0xFF, 0xFFFF]) + + raw_data = self.pack_data(CANOPEN_TPDO4 + 2, data_structs, 0xFFFFFF, 0xFFFFFFFF) + parsed_data = CanViewer.unpack_data(CANOPEN_TPDO4 + 2, data_structs, raw_data) + self.assertListEqual(parsed_data, [0xFFFFFF, 0xFFFFFFFF]) + + # See: http://python-future.org/compatible_idioms.html#long-integers + from past.builtins import long + self.assertTrue(all(isinstance(d, (int, long)) for d in parsed_data)) + + # Make sure that the ValueError exception is raised + with self.assertRaises(ValueError): + self.pack_data(0x101, data_structs, 1, 2, 3, 4) + + with self.assertRaises(ValueError): + CanViewer.unpack_data(0x102, data_structs, b'\x01\x02\x03\x04\x05\x06\x07\x08') + + def test_parse_args(self): + parsed_args, _, _ = parse_args(['-b', '250000']) + self.assertEqual(parsed_args.bitrate, 250000) + + parsed_args, _, _ = parse_args(['--bitrate', '500000']) + self.assertEqual(parsed_args.bitrate, 500000) + + parsed_args, _, _ = parse_args(['-c', 'can0']) + self.assertEqual(parsed_args.channel, 'can0') + + parsed_args, _, _ = parse_args(['--channel', 'PCAN_USBBUS1']) + self.assertEqual(parsed_args.channel, 'PCAN_USBBUS1') + + parsed_args, _, data_structs = parse_args(['-d', '100: