diff --git a/.appveyor.yml b/.appveyor.yml index 500c71320..aa7c12293 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -15,7 +15,7 @@ install: - set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH% # We need to install the python-can library itself including the dependencies - - "python -m pip install .[test,neovi]" + - "python -m pip install .[test,neovi,mf4]" build: off diff --git a/.travis.yml b/.travis.yml index e0a4b0b3f..a83d461af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -74,6 +74,7 @@ jobs: # -a Write all files # -n nitpicky - python -m sphinx -an doc build + - stage: linter name: "Linter Checks" python: "3.7" @@ -85,6 +86,8 @@ jobs: # Slowly enable all pylint warnings by adding addressed classes of # warnings to the .pylintrc-wip file to prevent them from being # re-introduced + + # check the entire main codebase - pylint --rcfile=.pylintrc-wip can/**.py # check setup.py @@ -116,6 +119,7 @@ jobs: can/io/**.py scripts/**.py examples/**.py + - stage: linter name: "Formatting Checks" python: "3.7" @@ -123,6 +127,7 @@ jobs: - travis_retry pip install -r requirements-lint.txt script: - black --check --verbose . + - stage: deploy name: "PyPi Deployment" python: "3.7" diff --git a/README.rst b/README.rst index 0214f2d4b..539267a37 100644 --- a/README.rst +++ b/README.rst @@ -67,7 +67,7 @@ Features - 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 +- many different loggers and readers supporting playback: ASC (CANalyzer format), BLF (Binary Logging Format by Vector), CSV, SQLite, Canutils log and MF4 (Measurement Data Format v4 by ASAM) - 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 buses (see the `docs `__) diff --git a/can/__init__.py b/can/__init__.py index 457546307..6ce56c9d9 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -28,6 +28,11 @@ class CanError(IOError): from .io import CSVWriter, CSVReader from .io import SqliteWriter, SqliteReader +try: + from .io import MF4Writer, MF4Reader +except ImportError: + pass + from .util import set_logging_level from .message import Message diff --git a/can/io/__init__.py b/can/io/__init__.py index 53389e91b..e09f969ad 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -14,3 +14,8 @@ from .csv import CSVWriter, CSVReader from .sqlite import SqliteReader, SqliteWriter from .printer import Printer + +try: + from .mf4 import MF4Writer, MF4Reader +except ImportError: + pass diff --git a/can/io/logger.py b/can/io/logger.py index 3c9cf5e46..9582ee5be 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -1,7 +1,10 @@ +# coding: utf-8 + """ See the :class:`Logger` class. """ +import logging import pathlib import typing @@ -16,6 +19,12 @@ from .sqlite import SqliteWriter from .printer import Printer +try: + from .mf4 import MF4Writer +except ImportError: + # be careful when using MF4Writer, it might NameError + pass + class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method """ @@ -27,6 +36,7 @@ class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method * .csv: :class:`can.CSVWriter` * .db: :class:`can.SqliteWriter` * .log :class:`can.CanutilsLogWriter` + * .mf4 :class:`can.MF4Writer` * .txt :class:`can.Printer` The **filename** may also be *None*, to fall back to :class:`can.Printer`. @@ -59,6 +69,11 @@ def __new__( ".log": CanutilsLogWriter, ".txt": Printer, } + try: + lookup[".mf4"] = MF4Writer + except NameError: + pass + suffix = pathlib.PurePath(filename).suffix try: return lookup[suffix](filename, *args, **kwargs) diff --git a/can/io/mf4.py b/can/io/mf4.py new file mode 100644 index 000000000..4ad85ce8a --- /dev/null +++ b/can/io/mf4.py @@ -0,0 +1,401 @@ +""" +Contains handling of MF4 logging files. + +MF4 files represent Measurement Data Format (MDF) version 4 as specified by +the ASAM MDF standard (see https://www.asam.net/standards/detail/mdf/) +""" + +from datetime import datetime +from pathlib import Path +import logging + +from ..message import Message +from ..listener import Listener +from ..util import channel2int +from .generic import BaseIOHandler + +try: + from asammdf import MDF4, Signal + import numpy as np + + ASAMMDF_AVAILABLE = True + +except ImportError as error: + ASAMMDF_AVAILABLE = False + raise error + + +CAN_MSG_EXT = 0x80000000 +CAN_ID_MASK = 0x1FFFFFFF + +STD_DTYPE = np.dtype( + [ + ("CAN_DataFrame.BusChannel", "=3.0"], + "mf4": ["asammdf>=5.5.0", "numpy>=1.16.0", "Cython"], "serial": ["pyserial~=3.0"], "neovi": ["python-ics>=2.12", "filelock"], } -tests_require = [ +extras_require["test"] = [ "pytest~=4.3", "pytest-timeout~=1.3", "pytest-cov~=2.6", @@ -40,7 +44,9 @@ "hypothesis", ] + extras_require["serial"] -extras_require["test"] = tests_require +# see GitHub issue #696: MF4 does not run on PyPy +if IS_CPYTHON: + extras_require["test"] += extras_require["mf4"] # Check for 'pytest-runner' only if setup.py was invoked with 'test'. # This optimizes setup.py for cases when pytest-runner is not needed, @@ -104,10 +110,10 @@ "aenum", 'windows-curses;platform_system=="Windows"', "filelock", - "mypy_extensions >= 0.4.0, < 0.5.0", + "mypy_extensions~=0.4.0", 'pywin32;platform_system=="Windows"', ], setup_requires=pytest_runner, extras_require=extras_require, - tests_require=tests_require, + tests_require=extras_require["test"], ) diff --git a/test/logformats_test.py b/test/logformats_test.py index 9983b0ecb..46dbcde67 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -142,6 +142,7 @@ def test_path_like_explicit_stop(self): 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) @@ -412,6 +413,28 @@ def _setup_instance(self): ) +try: + from can import MF4Writer, MF4Reader +except ImportError: + # seems to unsupported on this platform + # see GitHub issue #696: MF4 does not run on PyPy + pass +else: + + class TestMF4FileFormat(ReaderWriterTest): + """Tests can.MF4Writer and can.MF4Reader""" + + def _setup_instance(self): + super()._setup_instance_helper( + MF4Writer, + MF4Reader, + binary_file=True, + check_comments=False, + preserves_channel=False, + adds_default_channel=0, + ) + + class TestSqliteDatabaseFormat(ReaderWriterTest): """Tests can.SqliteWriter and can.SqliteReader"""