diff --git a/.travis.yml b/.travis.yml index b45e17d95..6525cea2d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ python: - "2.7" - "3.6" - "3.7" - - 3.8-dev + - "3.8" - nightly # PyPy: - pypy # Python 2.7 @@ -25,6 +25,7 @@ env: install: - if [[ "$TEST_SOCKETCAN" ]]; then sudo bash test/open_vcan.sh ; fi - travis_retry pip install .[test] + - pip freeze script: - | @@ -52,6 +53,14 @@ jobs: # Unit Testing Stage + # testing socketcan on Trusty & Python 2.7, since it is not available on Xenial + - stage: test + name: Socketcan + os: linux + dist: trusty + python: "2.7" + sudo: required + env: TEST_SOCKETCAN=TRUE # testing socketcan on Trusty & Python 3.6, since it is not available on Xenial - stage: test name: Socketcan @@ -88,6 +97,9 @@ jobs: - stage: deploy name: "PyPi Deployment" python: "3.7" + before_install: + - travis_retry pip install -U pip + - travis_retry pip install -U wheel setuptools deploy: provider: pypi user: hardbyte diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 2f50dca8d..7899d50b6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,57 @@ -Version 3.2.0 +Version 3.3.4 +==== + +Last call for Python2 support. + +* #916 Vector: Skip channels without CAN support +* #846 Use inter-process mutex to prevent concurrent neoVI device open. +* #901 Fix iteration in Bus.stop_all_periodic_tasks +* #850 Fix socket.error is a deprecated alias of OSError used on Python versions lower than 3.3. +* #879 Updating incorrect api documentation. +* #885 Fix recursion message in Message.__getattr__ +* #845 Fix socketcan issue + + +Version 3.3.3 +==== + +Backported fixes from 4.x development branch which targets Python 3. + +* #798 Backport caching msg.data value in neovi interface. +* #796 Fix Vector CANlib treatment of empty app name. +* #771 Handle empty CSV file. +* #741 ASCII reader can now handle FD frames. +* #740 Exclude test packages from distribution. +* #713 RTR crash fix in canutils log reader parsing RTR frames. +* #701 Skip J1939 messages in ASC Reader. +* #690 Exposes a configuration option to allow the CAN message player to send error frames + (and sets the default to not send error frames). +* #638 Fixes the semantics provided by periodic tasks in SocketCAN interface. +* #628 Avoid padding CAN_FD_MESSAGE_64 objects to 4 bytes. +* #617 Fixes the broken CANalyst-II interface. +* #605 Socketcan BCM status fix. + + +Version 3.3.2 +==== + +Minor bug fix release addressing issue in PCAN RTR. + +Version 3.3.1 +==== + +Minor fix to setup.py to only require pytest-runner when necessary. + +Version 3.3.0 ==== +* Adding CAN FD 64 frame support to blf reader +* Updates to installation instructions +* Clean up bits generator in PCAN interface #588 +* Minor fix to use latest tools when building wheels on travis. + +Version 3.2.0 +==== Major features -------------- diff --git a/can/__init__.py b/can/__init__.py index a612363ae..0d310e436 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging -__version__ = "3.2.0" +__version__ = "3.3.5-dev1" log = logging.getLogger('can') diff --git a/can/bus.py b/can/bus.py index 2b36b3c57..2b3044cf1 100644 --- a/can/bus.py +++ b/can/bus.py @@ -247,13 +247,21 @@ def _send_periodic_internal(self, msg, period, duration=None): return task def stop_all_periodic_tasks(self, remove_tasks=True): - """Stop sending any messages that were started using bus.send_periodic + """Stop sending any messages that were started using **bus.send_periodic**. + + .. note:: + The result is undefined if a single task throws an exception while being stopped. :param bool remove_tasks: Stop tracking the stopped tasks. """ for task in self._periodic_tasks: - task.stop(remove_task=remove_tasks) + # we cannot let `task.stop()` modify `self._periodic_tasks` while we are + # iterating over it (#634) + task.stop(remove_task=False) + + if remove_tasks: + self._periodic_tasks = [] def __iter__(self): """Allow iteration on messages as they are received. diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index 35f240a66..859fdca9a 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -1,3 +1,4 @@ +import warnings from ctypes import * import logging import platform @@ -66,17 +67,19 @@ class VCI_CAN_OBJ(Structure): class CANalystIIBus(BusABC): - def __init__(self, channel, device=0, baud=None, Timing0=None, Timing1=None, can_filters=None): + def __init__( + self, channel, device=0, bitrate=None, baud=None, Timing0=None, Timing1=None, can_filters=None, **kwargs + ): """ :param channel: channel number :param device: device number - :param baud: baud rate + :param baud: baud rate. Renamed to bitrate in next release. :param Timing0: customize the timing register if baudrate is not specified :param Timing1: :param can_filters: filters for packet """ - super(CANalystIIBus, self).__init__(channel, can_filters) + super(CANalystIIBus, self).__init__(channel, can_filters, **kwargs) if isinstance(channel, (list, tuple)): self.channels = channel @@ -91,10 +94,15 @@ def __init__(self, channel, device=0, baud=None, Timing0=None, Timing1=None, can self.channel_info = "CANalyst-II: device {}, channels {}".format(self.device, self.channels) if baud is not None: + warnings.warn('Argument baud will be deprecated in version 4, use bitrate instead', + PendingDeprecationWarning) + bitrate = baud + + if bitrate is not None: try: - Timing0, Timing1 = TIMING_DICT[baud] + Timing0, Timing1 = TIMING_DICT[bitrate] except KeyError: - raise ValueError("Baudrate is not supported") + raise ValueError("Bitrate is not supported") if Timing0 is None or Timing1 is None: raise ValueError("Timing registers are not set") diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 4baee6177..314967708 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -11,6 +11,8 @@ """ import logging +import os +import tempfile from collections import deque from can import Message, CanError, BusABC @@ -27,6 +29,35 @@ ics = None +try: + from filelock import FileLock +except ImportError as ie: + + logger.warning( + "Using ICS NeoVi can backend without the " + "filelock module installed may cause some issues!: %s", + ie, + ) + + class FileLock: + """Dummy file lock that does not actually do anything""" + + def __init__(self, lock_file, timeout=-1): + self._lock_file = lock_file + self.timeout = timeout + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + return None + + +# Use inter-process mutex to prevent concurrent device open. +# When neoVI server is enabled, there is an issue with concurrent device open. +open_lock = FileLock(os.path.join(tempfile.gettempdir(), "neovi.lock")) + + class ICSApiError(CanError): """ Indicates an error with the ICS API. @@ -118,18 +149,28 @@ def __init__(self, channel, can_filters=None, **kwargs): type_filter = kwargs.get('type_filter') serial = kwargs.get('serial') self.dev = self._find_device(type_filter, serial) - ics.open_device(self.dev) - if 'bitrate' in kwargs: - for channel in self.channels: - ics.set_bit_rate(self.dev, kwargs.get('bitrate'), channel) + with open_lock: + ics.open_device(self.dev) - fd = kwargs.get('fd', False) - if fd: - if 'data_bitrate' in kwargs: + try: + if "bitrate" in kwargs: for channel in self.channels: - ics.set_fd_bit_rate( - self.dev, kwargs.get('data_bitrate'), channel) + ics.set_bit_rate(self.dev, kwargs.get("bitrate"), channel) + + if kwargs.get("fd", False): + if "data_bitrate" in kwargs: + for channel in self.channels: + ics.set_fd_bit_rate( + self.dev, kwargs.get("data_bitrate"), channel + ) + except ics.RuntimeError as re: + logger.error(re) + err = ICSApiError(*ics.get_last_api_error(self.dev)) + try: + self.shutdown() + finally: + raise err self._use_system_timestamp = bool( kwargs.get('use_system_timestamp', False) @@ -334,11 +375,12 @@ def send(self, msg, timeout=None): flag3 |= ics.SPY_STATUS3_CANFD_ESI message.ArbIDOrHeader = msg.arbitration_id - message.NumberBytesData = len(msg.data) - message.Data = tuple(msg.data[:8]) - if msg.is_fd and len(msg.data) > 8: + msg_data = msg.data + message.NumberBytesData = len(msg_data) + message.Data = tuple(msg_data[:8]) + if msg.is_fd and len(msg_data) > 8: message.ExtraDataPtrEnabled = 1 - message.ExtraDataPtr = tuple(msg.data) + message.ExtraDataPtr = tuple(msg_data) message.StatusBitField = flag0 message.StatusBitField2 = 0 message.StatusBitField3 = flag3 diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 84c8751c1..eac867344 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -328,10 +328,12 @@ def __init__(self, channel, can_filters=None, **kwargs): else: raise VCIDeviceNotFoundError("Unique HW ID {} not connected or not available.".format(UniqueHardwareId)) else: - if (UniqueHardwareId is None) or (self._device_info.UniqueHardwareId.AsChar == bytes(UniqueHardwareId, 'ascii')): + if (UniqueHardwareId is None) or ( + self._device_info.UniqueHardwareId.AsChar == UniqueHardwareId.encode("ascii")): break else: - log.debug("Ignoring IXXAT with hardware id '%s'.", self._device_info.UniqueHardwareId.AsChar.decode("ascii")) + log.debug("Ignoring IXXAT with hardware id '%s'.", + self._device_info.UniqueHardwareId.AsChar.decode("ascii")) _canlib.vciEnumDeviceClose(self._device_handle) _canlib.vciDeviceOpen(ctypes.byref(self._device_info.VciObjectId), ctypes.byref(self._device_handle)) log.info("Using unique HW ID %s", self._device_info.UniqueHardwareId.AsChar) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 864308bab..f062d62aa 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -385,9 +385,7 @@ def send(self, msg, timeout=None): CANMsg.MSGTYPE = msgType # if a remote frame will be sent, data bytes are not important. - if msg.is_remote_frame: - CANMsg.MSGTYPE = msgType.value | PCAN_MESSAGE_RTR.value - else: + if not msg.is_remote_frame: # copy data for i in range(CANMsg.LEN): CANMsg.DATA[i] = msg.data[i] diff --git a/can/interfaces/socketcan/constants.py b/can/interfaces/socketcan/constants.py index b56eaae64..1fa6fdf72 100644 --- a/can/interfaces/socketcan/constants.py +++ b/can/interfaces/socketcan/constants.py @@ -9,8 +9,9 @@ CAN_EFF_FLAG = 0x80000000 # BCM opcodes -CAN_BCM_TX_SETUP = 1 -CAN_BCM_TX_DELETE = 2 +CAN_BCM_TX_SETUP = 1 +CAN_BCM_TX_DELETE = 2 +CAN_BCM_TX_READ = 3 # BCM flags SETTIMER = 0x0001 diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 633c87b22..4bb989dd7 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -346,8 +346,40 @@ def _tx_setup(self, message): count = 0 ival1 = 0 ival2 = self.period - header = build_bcm_transmit_header(self.can_id_with_flags, count, ival1, - ival2, self.flags) + + # First do a TX_READ before creating a new task, and check if we get + # EINVAL. If so, then we are referring to a CAN message with the same + # ID + check_header = build_bcm_header( + opcode=CAN_BCM_TX_READ, + flags=0, + count=0, + ival1_seconds=0, + ival1_usec=0, + ival2_seconds=0, + ival2_usec=0, + can_id=self.can_id_with_flags, + nframes=0, + ) + try: + self.bcm_socket.send(check_header) + except (socket.error, OSError) as e: + if e.errno != errno.EINVAL: + raise e + else: + raise ValueError( + "A periodic Task for Arbitration ID {} has already been created".format( + message.arbitration_id + ) + ) + + header = build_bcm_transmit_header( + self.can_id_with_flags, + count, + ival1, + ival2, + self.flags + ) frame = build_can_frame(message) log.debug("Sending BCM command") send_bcm(self.bcm_socket, header + frame) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 251b9fa56..0864ecefe 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -87,7 +87,7 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, else: # Assume comma separated string of channels self.channels = [int(ch.strip()) for ch in channel.split(',')] - self._app_name = app_name.encode() if app_name is not None else '' + self._app_name = app_name.encode() if app_name is not None else b'' self.channel_info = 'Application %s: %s' % ( app_name, ', '.join('CAN %d' % (ch + 1) for ch in self.channels)) @@ -387,6 +387,8 @@ def _detect_available_configs(): channel_configs = get_channel_configs() LOG.info('Found %d channels', len(channel_configs)) for channel_config in channel_configs: + if not channel_config.channelBusCapabilities & vxlapi.XL_BUS_ACTIVE_CAP_CAN: + continue LOG.info('Channel index %d: %s', channel_config.channelIndex, channel_config.name.decode('ascii')) diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py index ae87706c4..461a62197 100644 --- a/can/interfaces/vector/vxlapi.py +++ b/can/interfaces/vector/vxlapi.py @@ -63,6 +63,7 @@ XL_INTERFACE_VERSION = 3 XL_INTERFACE_VERSION_V4 = 4 +XL_BUS_ACTIVE_CAP_CAN = XL_BUS_TYPE_CAN << 16 XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT = 0x80000000 # structure for XL_RECEIVE_MSG, XL_TRANSMIT_MSG diff --git a/can/io/asc.py b/can/io/asc.py index 3ed50f04a..60f845bd0 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -28,7 +28,8 @@ class ASCReader(BaseIOHandler): """ - Iterator of CAN messages from a ASC logging file. + Iterator of CAN messages from a ASC logging file. Meta data (comments, + bus statistics, J1939 Transport Protocol messages) is ignored. TODO: turn relative timestamps back to absolute form """ @@ -58,9 +59,13 @@ def __iter__(self): temp = line.strip() if not temp or not temp[0].isdigit(): continue - + is_fd = False try: timestamp, channel, dummy = temp.split(None, 2) # , frameType, dlc, frameData + if channel == "CANFD": + timestamp, _, channel, _, dummy = temp.split(None, 4) + is_fd = True + except ValueError: # we parsed an empty comment continue @@ -77,7 +82,10 @@ def __iter__(self): channel=channel) yield msg - elif not isinstance(channel, int) or dummy.strip()[0:10].lower() == 'statistic:': + elif (not isinstance(channel, int) + or dummy.strip()[0:10].lower() == 'statistic:' + or dummy.split(None, 1)[0] == "J1939TP" + ): pass elif dummy[-1:].lower() == 'r': @@ -91,16 +99,32 @@ def __iter__(self): yield msg else: + brs = None + esi = None + data_length = 0 try: - # this only works if dlc > 0 and thus data is availabe - can_id_str, _, _, dlc, data = dummy.split(None, 4) + # this only works if dlc > 0 and thus data is available + if not is_fd: + can_id_str, _, _, dlc, data = dummy.split(None, 4) + else: + can_id_str, frame_name, brs, esi, dlc, data_length, data = dummy.split( + None, 6 + ) + if frame_name.isdigit(): + # Empty frame_name + can_id_str, brs, esi, dlc, data_length, data = dummy.split( + None, 5 + ) except ValueError: # but if not, we only want to get the stuff up to the dlc can_id_str, _, _, dlc = dummy.split(None, 3) # and we set data to an empty sequence manually data = '' - - dlc = int(dlc) + dlc = int(dlc, 16) + if is_fd: + # For fd frames, dlc and data length might not be equal and + # data_length is the actual size of the data + dlc = int(data_length) frame = bytearray() data = data.split() for byte in data[0:dlc]: @@ -115,7 +139,10 @@ def __iter__(self): is_remote_frame=False, dlc=dlc, data=frame, - channel=channel + is_fd=is_fd, + channel=channel, + bitrate_switch=is_fd and brs == "1", + error_state_indicator=is_fd and esi == "1", ) self.stop() diff --git a/can/io/blf.py b/can/io/blf.py index d162fdebc..91f2945c9 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -195,8 +195,11 @@ def __iter__(self): raise BLFParseError() obj_size = header[3] + obj_type = header[4] # Calculate position of next object - next_pos = pos + obj_size + (obj_size % 4) + next_pos = pos + obj_size + if obj_type != CAN_FD_MESSAGE_64: + next_pos += obj_size % 4 if next_pos > len(data): # Object continues in next log container break @@ -222,7 +225,6 @@ def __iter__(self): factor = 1e-9 timestamp = timestamp * factor + self.start_timestamp - obj_type = header[4] # Both CAN message types have the same starting content if obj_type in (CAN_MESSAGE, CAN_MESSAGE2): (channel, flags, dlc, can_id, diff --git a/can/io/canutils.py b/can/io/canutils.py index 69c0227a4..b5cec0cb5 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -62,7 +62,7 @@ def __iter__(self): else: isExtended = False canId = int(canId, 16) - + dataBin = None if data and data[0].lower() == 'r': isRemoteFrame = True if len(data) > 1: diff --git a/can/io/csv.py b/can/io/csv.py index 92f841f8f..32e34d0fb 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -91,7 +91,11 @@ def __init__(self, file): def __iter__(self): # skip the header line - next(self.file) + try: + next(self.file) + except StopIteration: + # don't crash on a file with only a header + return for line in self.file: diff --git a/can/message.py b/can/message.py index f85218fc0..84f675183 100644 --- a/can/message.py +++ b/can/message.py @@ -53,7 +53,9 @@ 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__ + # this method is only called if the attribute was not found elsewhere, like in __slots_ + if key not in self.__slots__: + raise AttributeError try: warnings.warn("Custom attributes of messages are deprecated and will be removed in 4.0", DeprecationWarning) return self._dict[key] @@ -110,6 +112,7 @@ def __init__(self, timestamp=0.0, arbitration_id=0, is_extended_id=None, if is_extended_id is not None: self.is_extended_id = is_extended_id else: + # Default behaviour is to create extended id messages self.is_extended_id = True if extended_id is None else extended_id self.is_remote_frame = is_remote_frame diff --git a/can/player.py b/can/player.py index c712f1714..2b1864a36 100644 --- a/can/player.py +++ b/can/player.py @@ -47,6 +47,12 @@ def main(): help='''Ignore timestamps (send all frames immediately with minimum gap between frames)''', action='store_false') + parser.add_argument( + "--error-frames", + help="Also send error frames to the interface.", + action="store_true", + ) + 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, @@ -68,6 +74,8 @@ def main(): logging_level_name = ['critical', 'error', 'warning', 'info', 'debug', 'subdebug'][min(5, verbosity)] can.set_logging_level(logging_level_name) + error_frames = results.error_frames + config = {"single_handle": True} if results.interface: config["interface"] = results.interface @@ -84,6 +92,8 @@ def main(): try: for m in in_sync: + if m.is_error_frame and not error_frames: + continue if verbosity >= 3: print(m) bus.send(m) diff --git a/doc/configuration.rst b/doc/configuration.rst index dda2ace2a..230eec1bd 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -19,7 +19,7 @@ the **interface** and **channel** before importing from ``can.interfaces``. can.rc['interface'] = 'socketcan' can.rc['channel'] = 'vcan0' can.rc['bitrate'] = 500000 - from can.interfaces.interface import Bus + from can.interface import Bus bus = Bus() @@ -126,3 +126,7 @@ Lookup table of interface names: +---------------------+-------------------------------------+ | ``"virtual"`` | :doc:`interfaces/virtual` | +---------------------+-------------------------------------+ +| ``"canalystii"`` | :doc:`interfaces/canalystii` | ++---------------------+-------------------------------------+ +| ``"systec"`` | :doc:`interfaces/systec` | ++---------------------+-------------------------------------+ diff --git a/doc/development.rst b/doc/development.rst index 602e4e347..fdb5bcf25 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -73,8 +73,10 @@ The modules in ``python-can`` are: +---------------------------------+------------------------------------------------------+ -Creating a new Release ----------------------- +Process for creating a new Release +---------------------------------- + +Note many of these steps are carried out by the CI system on creating a tag in git. - Release from the ``master`` branch. - Update the library version in ``__init__.py`` using `semantic versioning `__. @@ -84,8 +86,9 @@ Creating a new Release - 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``. +- Ensure the ``setuptools`` and ``wheel`` tools are up to date: ``pip install -U setuptools wheel``. - 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``. +- [Optionally] 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. diff --git a/setup.py b/setup.py index c600b7215..898f54c16 100644 --- a/setup.py +++ b/setup.py @@ -11,6 +11,7 @@ from os.path import isfile, join import re import logging +import sys from setuptools import setup, find_packages logging.basicConfig(level=logging.WARNING) @@ -25,22 +26,32 @@ # Dependencies extras_require = { 'serial': ['pyserial~=3.0'], - 'neovi': ['python-ics>=2.12'] + 'neovi': ['python-ics>=2.12', 'filelock'] } tests_require = [ 'mock~=2.0', - 'pytest~=4.3', + 'pytest~=4.6', 'pytest-timeout~=1.3', - 'pytest-cov~=2.6', + 'pytest-cov~=2.8', + # coveragepy==5.0 fails with `Safety level may not be changed inside a transaction` + # on python 3.6 on MACOS + 'coverage<5', 'codecov~=2.0', 'future', 'six', - 'hypothesis' + 'hypothesis~=4.56' ] + extras_require['serial'] extras_require['test'] = tests_require +# Check for 'pytest-runner' only if setup.py was invoked with 'test'. +# This optimizes setup.py for cases when pytest-runner is not needed, +# using the approach that is suggested upstream. +# +# See https://pypi.org/project/pytest-runner/#conditional-requirement +needs_pytest = {"pytest", "test", "ptr"}.intersection(sys.argv) +pytest_runner = ["pytest-runner"] if needs_pytest else [] setup( # Description @@ -55,6 +66,7 @@ "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", @@ -78,7 +90,7 @@ # Code version=version, - packages=find_packages(exclude=["test", "doc", "scripts", "examples"]), + packages=find_packages(exclude=["test*", "doc", "scripts", "examples"]), scripts=list(filter(isfile, (join("scripts/", f) for f in listdir("scripts/")))), # Author @@ -104,7 +116,7 @@ 'typing;python_version<"3.5"', 'windows-curses;platform_system=="Windows"', ], - setup_requires=["pytest-runner"], + setup_requires=pytest_runner, extras_require=extras_require, tests_require=tests_require ) diff --git a/test/data/logfile.asc b/test/data/logfile.asc index 4b7c64363..b855811a2 100644 --- a/test/data/logfile.asc +++ b/test/data/logfile.asc @@ -1,18 +1,28 @@ -date Sam Sep 30 15:06:13.191 2017 -base hex timestamps absolute -internal events logged -// version 9.0.0 -Begin Triggerblock Sam Sep 30 15:06:13.191 2017 - 0.000000 Start of measurement - 0.015991 CAN 1 Status:chip status error passive - TxErr: 132 RxErr: 0 - 0.015991 CAN 2 Status:chip status error active - 1.015991 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% - 1.015991 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% - 2.015992 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% - 17.876707 CAN 1 Status:chip status error passive - TxErr: 131 RxErr: 0 - 17.876708 1 6F9 Rx d 8 05 0C 00 00 00 00 00 00 Length = 240015 BitCount = 124 ID = 1785 - 17.876976 1 6F8 Rx d 8 FF 00 0C FE 00 00 00 00 Length = 239910 BitCount = 124 ID = 1784 - 18.015997 1 Statistic: D 2 R 0 XD 0 XR 0 E 0 O 0 B 0.04% - 113.016026 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% - 113.016026 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% -End TriggerBlock +date Sam Sep 30 15:06:13.191 2017 +base hex timestamps absolute +internal events logged +// version 9.0.0 +Begin Triggerblock Sam Sep 30 15:06:13.191 2017 + 0.000000 Start of measurement + 0.015991 CAN 1 Status:chip status error passive - TxErr: 132 RxErr: 0 + 0.015991 CAN 2 Status:chip status error active + 1.015991 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% + 1.015991 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% + 2.015992 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% + 3.098426 1 18EBFF00x Rx d 8 01 A0 0F A6 60 3B D1 40 Length = 273910 BitCount = 141 ID = 418119424x + 3.148421 1 18EBFF00x Rx d 8 02 1F DE 80 25 DF C0 2B Length = 271910 BitCount = 140 ID = 418119424x + 3.197693 1 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x + 3.248765 1 18EBFF00x Rx d 8 04 00 4B FF FF FF FF FF Length = 283910 BitCount = 146 ID = 418119424x + 3.297743 1 J1939TP FEE3p 6 0 0 - Rx d 23 A0 0F A6 60 3B D1 40 1F DE 80 25 DF C0 2B E1 00 4B FF FF 3C 0F 00 4B FF FF FF FF FF FF FF FF FF FF FF FF + 17.876707 CAN 1 Status:chip status error passive - TxErr: 131 RxErr: 0 + 17.876708 1 6F9 Rx d 8 05 0C 00 00 00 00 00 00 Length = 240015 BitCount = 124 ID = 1785 + 17.876976 1 6F8 Rx d 8 FF 00 0C FE 00 00 00 00 Length = 239910 BitCount = 124 ID = 1784 + 18.015997 1 Statistic: D 2 R 0 XD 0 XR 0 E 0 O 0 B 0.04% + 20.105214 2 18EBFF00x Rx d 8 01 A0 0F A6 60 3B D1 40 Length = 273925 BitCount = 141 ID = 418119424x + 20.155119 2 18EBFF00x Rx d 8 02 1F DE 80 25 DF C0 2B Length = 272152 BitCount = 140 ID = 418119424x + 20.204671 2 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x + 20.248887 2 18EBFF00x Rx d 8 04 00 4B FF FF FF FF FF Length = 283925 BitCount = 146 ID = 418119424x + 20.305233 2 J1939TP FEE3p 6 0 0 - Rx d 23 A0 0F A6 60 3B D1 40 1F DE 80 25 DF C0 2B E1 00 4B FF FF 3C 0F 00 4B FF FF FF FF FF FF FF FF FF FF FF FF + 113.016026 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% + 113.016026 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% +End TriggerBlock diff --git a/test/logformats_test.py b/test/logformats_test.py index d9551e5d6..41bf2e9c4 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -315,7 +315,7 @@ class TestAscFileFormat(ReaderWriterTest): def _setup_instance(self): super(TestAscFileFormat, self)._setup_instance_helper( can.ASCWriter, can.ASCReader, - check_fd=False, + check_fd=True, check_comments=True, preserves_channel=False, adds_default_channel=0 ) diff --git a/test/test_cyclic_socketcan.py b/test/test_cyclic_socketcan.py new file mode 100644 index 000000000..831264bf4 --- /dev/null +++ b/test/test_cyclic_socketcan.py @@ -0,0 +1,226 @@ +""" +This module tests multiple message cyclic send tasks. +""" +import unittest + +import time +import can + +from .config import TEST_INTERFACE_SOCKETCAN + + +@unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan") +class CyclicSocketCan(unittest.TestCase): + BITRATE = 500000 + TIMEOUT = 0.1 + + INTERFACE_1 = "socketcan" + CHANNEL_1 = "vcan0" + INTERFACE_2 = "socketcan" + CHANNEL_2 = "vcan0" + + PERIOD = 1.0 + + DELTA = 0.01 + + def _find_start_index(self, tx_messages, message): + """ + :param tx_messages: + The list of messages that were passed to the periodic backend + :param message: + The message whose data we wish to match and align to + + :returns: start index in the tx_messages + """ + start_index = -1 + for index, tx_message in enumerate(tx_messages): + if tx_message.data == message.data: + start_index = index + break + return start_index + + def setUp(self): + self._send_bus = can.Bus( + interface=self.INTERFACE_1, channel=self.CHANNEL_1, bitrate=self.BITRATE + ) + self._recv_bus = can.Bus( + interface=self.INTERFACE_2, channel=self.CHANNEL_2, bitrate=self.BITRATE + ) + + def tearDown(self): + self._send_bus.shutdown() + self._recv_bus.shutdown() + + def test_cyclic_initializer_message(self): + message = can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + + task = self._send_bus.send_periodic(message, self.PERIOD) + self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) + + # Take advantage of kernel's queueing mechanisms + time.sleep(4 * self.PERIOD) + task.stop() + + for _ in range(4): + tx_message = message + rx_message = self._recv_bus.recv(self.TIMEOUT) + + self.assertIsNotNone(rx_message) + self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) + self.assertEqual(tx_message.dlc, rx_message.dlc) + self.assertEqual(tx_message.data, rx_message.data) + self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) + self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) + self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) + self.assertEqual(tx_message.is_fd, rx_message.is_fd) + + def test_create_same_id_raises_exception(self): + messages_a = can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + + messages_b = can.Message( + arbitration_id=0x401, + data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], + is_extended_id=False, + ) + + task_a = self._send_bus.send_periodic(messages_a, 1) + self.assertIsInstance(task_a, can.broadcastmanager.CyclicSendTaskABC) + + # The second one raises a ValueError when we attempt to create a new + # Task, since it has the same arbitration ID. + with self.assertRaises(ValueError): + task_b = self._send_bus.send_periodic(messages_b, 1) + + def test_modify_data_message(self): + message_odd = can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + message_even = can.Message( + arbitration_id=0x401, + data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], + is_extended_id=False, + ) + task = self._send_bus.send_periodic(message_odd, self.PERIOD) + self.assertIsInstance(task, can.broadcastmanager.ModifiableCyclicTaskABC) + + results_odd = [] + results_even = [] + for _ in range(1 * 4): + result = self._recv_bus.recv(self.PERIOD * 2) + if result: + results_odd.append(result) + + task.modify_data(message_even) + for _ in range(1 * 4): + result = self._recv_bus.recv(self.PERIOD * 2) + if result: + results_even.append(result) + + task.stop() + + # Now go through the partitioned results and assert that they're equal + for rx_index, rx_message in enumerate(results_even): + tx_message = message_even + + self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) + self.assertEqual(tx_message.dlc, rx_message.dlc) + self.assertEqual(tx_message.data, rx_message.data) + self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) + self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) + self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) + self.assertEqual(tx_message.is_fd, rx_message.is_fd) + + if rx_index != 0: + prev_rx_message = results_even[rx_index - 1] + # Assert timestamps are within the expected period + self.assertTrue( + abs( + (rx_message.timestamp - prev_rx_message.timestamp) - self.PERIOD + ) + <= self.DELTA + ) + + for rx_index, rx_message in enumerate(results_odd): + tx_message = message_odd + + self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) + self.assertEqual(tx_message.dlc, rx_message.dlc) + self.assertEqual(tx_message.data, rx_message.data) + self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) + self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) + self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) + self.assertEqual(tx_message.is_fd, rx_message.is_fd) + + if rx_index != 0: + prev_rx_message = results_odd[rx_index - 1] + # Assert timestamps are within the expected period + self.assertTrue( + abs( + (rx_message.timestamp - prev_rx_message.timestamp) - self.PERIOD + ) + <= self.DELTA + ) + + def test_stop_all_periodic_tasks_and_remove_task(self): + message_a = can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + message_b = can.Message( + arbitration_id=0x402, + data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], + is_extended_id=False, + ) + message_c = can.Message( + arbitration_id=0x403, + data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33], + is_extended_id=False, + ) + + # Start Tasks + task_a = self._send_bus.send_periodic(message_a, self.PERIOD) + task_b = self._send_bus.send_periodic(message_b, self.PERIOD) + task_c = self._send_bus.send_periodic(message_c, self.PERIOD) + + self.assertIsInstance(task_a, can.broadcastmanager.ModifiableCyclicTaskABC) + self.assertIsInstance(task_b, can.broadcastmanager.ModifiableCyclicTaskABC) + self.assertIsInstance(task_c, can.broadcastmanager.ModifiableCyclicTaskABC) + + for _ in range(6): + _ = self._recv_bus.recv(self.PERIOD) + + # Stop all tasks and delete + self._send_bus.stop_all_periodic_tasks(remove_tasks=True) + + # Now wait for a few periods, after which we should definitely not + # receive any CAN messages + time.sleep(4 * self.PERIOD) + + # If we successfully deleted everything, then we will eventually read + # 0 messages. + successfully_stopped = False + for _ in range(6): + rx_message = self._recv_bus.recv(self.PERIOD) + + if rx_message is None: + successfully_stopped = True + break + self.assertTrue(successfully_stopped, "Still received messages after stopping") + + # None of the tasks should still be associated with the bus + self.assertEqual(0, len(self._send_bus._periodic_tasks)) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_message_class.py b/test/test_message_class.py index 85dbe8560..05ed14b2a 100644 --- a/test/test_message_class.py +++ b/test/test_message_class.py @@ -5,12 +5,15 @@ import sys from math import isinf, isnan from copy import copy, deepcopy +import pickle from hypothesis import given, settings, reproduce_failure import hypothesis.strategies as st from can import Message +from .message_helper import ComparingMessagesTestCase + class TestMessageClass(unittest.TestCase): """ @@ -70,7 +73,7 @@ def test_methods(self, **kwargs): # check copies and equalities if is_valid: - self.assertEqual(message, message) + self.assertEqual(message, message) normal_copy = copy(message) deep_copy = deepcopy(message) for other in (normal_copy, deep_copy, message): @@ -79,5 +82,31 @@ def test_methods(self, **kwargs): self.assertTrue(message.equals(other, timestamp_delta=0)) -if __name__ == '__main__': +class MessageSerialization(unittest.TestCase, ComparingMessagesTestCase): + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + ComparingMessagesTestCase.__init__( + self, allowed_timestamp_delta=0.016, preserves_channel=True + ) + + def test_serialization(self): + message = Message( + timestamp=1.0, + arbitration_id=0x401, + is_extended_id=False, + is_remote_frame=False, + is_error_frame=False, + channel=1, + dlc=6, + data=bytearray([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]), + is_fd=False, + ) + + serialized = pickle.dumps(message, -1) + deserialized = pickle.loads(serialized) + + self.assertMessageEqual(message, deserialized) + + +if __name__ == "__main__": unittest.main()