diff --git a/.gitignore b/.gitignore index 4ea43b2ae..7666d148a 100644 --- a/.gitignore +++ b/.gitignore @@ -55,7 +55,7 @@ coverage.xml *.log # Sphinx documentation -docs/_build/ +doc/_build/ # PyBuilder target/ diff --git a/.travis.yml b/.travis.yml index 1b5989ab4..5ccaa3ff7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,47 @@ language: python -sudo: false + python: + # CPython: - "2.7" - - "pypy" - - "pypy3" + - "3.3" - "3.4" - "3.5" - "3.6" - - "3.7-dev" + - "3.7-dev" # TODO: change to "3.7" once it gets released - "nightly" + # PyPy: + - "pypy" + - "pypy3" + +os: + - linux # Linux is officially supported and we test the library under + # many different Python verions (see "python: ..." above) + +# - 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) + +# - windows # Windows is not supported at all by Travis CI as of Feb. 2018 + +# Linux setup +dist: trusty +sudo: false + +matrix: + # see "os: ..." above + include: + - os: osx + python: "3.6-dev" + - os: osx + python: "3.7-dev" + - os: osx + python: "nightly" + + # allow all nighly builds to fail, since these python versions might be unstable + # we do not allow dev builds to fail, since these builds are stable enough + allow_failures: + - python: "nightly" + install: - travis_retry pip install . - travis_retry pip install -r requirements.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 000000000..06b6ef4ee --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,38 @@ +Version 2.1.0 (2018-02-17) +===== + +* Support for out of tree can interfaces with pluggy. +* Initial support for CAN-FD for socketcan_native and kvaser interfaces. +* Neovi interface now uses Intrepid Control Systems's own interface library. +* Improvements and new documentation for SQL reader/writer. +* Fix bug in neovi serial number decoding. +* Add testing on OSX to TravisCI +* Fix non english decoding error on pcan +* Other misc improvements and bug fixes + + +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: + +* New interfaces: + * Vector + * NI-CAN + * isCAN + * neoVI +* Simplified periodic send API with initial support for SocketCAN +* Protocols module including J1939 support removed +* Logger script moved to module `can.logger` +* New `can.player` script to replay log files +* BLF, ASC log file support added in new `can.io` module + +You can install from [PyPi](https://pypi.python.org/pypi/python-can/2.0.0) with pip: + +``` +pip install python-can==2.0.0 +``` + +The documentation for v2.0.0 is available at http://python-can.readthedocs.io/en/2.0.0/ diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 8408ccdc8..5e5ea882b 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -19,3 +19,5 @@ Giuseppe Corbelli Christian Sandberg Eduard Bröcker Boris Wenzlaff +Pierre-Luc Tessier Gagné +Felix Divo diff --git a/README.rst b/README.rst index 85d128b3d..8002997eb 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ python-can :alt: Documentation Status .. |build| image:: https://travis-ci.org/hardbyte/python-can.svg?branch=develop - :target: https://travis-ci.org/hardbyte/python-can + :target: https://travis-ci.org/hardbyte/python-can/branches :alt: CI Server for develop branch @@ -26,7 +26,7 @@ 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.3+ and runs on Mac, Linux and Windows. +The library supports Python 2.7, Python 3.3+ as well as PyPy and runs on Mac, Linux and Windows. You can find more information in the documentation, online at `python-can.readthedocs.org `__. @@ -46,3 +46,8 @@ questions and answers tagged with ``python+can``. Wherever we interact, we strive to follow the `Python Community Code of Conduct `__. + +Contributing +------------ + +See `doc/development.rst `__ for getting started. diff --git a/can/__init__.py b/can/__init__.py index a15e64999..71bc0f442 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -5,7 +5,7 @@ import logging -__version__ = "2.0.0" +__version__ = "2.1.0" log = logging.getLogger('can') @@ -22,7 +22,7 @@ class CanError(IOError): from can.io import BLFReader, BLFWriter from can.io import CanutilsLogReader, CanutilsLogWriter from can.io import CSVWriter -from can.io import SqliteWriter, SqlReader +from can.io import SqliteWriter, SqliteReader from can.util import set_logging_level diff --git a/can/bus.py b/can/bus.py index d6b2526f6..f42a4a149 100644 --- a/can/bus.py +++ b/can/bus.py @@ -1,10 +1,16 @@ # -*- coding: utf-8 -*- -from __future__ import print_function, absolute_import +""" +Contains the ABC bus implementation. +""" + +from __future__ import print_function, absolute_import import abc import logging import threading from can.broadcastmanager import ThreadBasedCyclicSendTask + + logger = logging.getLogger(__name__) @@ -17,7 +23,6 @@ class BusABC(object): As well as setting the `channel_info` attribute to a string describing the interface. - """ #: a string describing the underlying bus channel @@ -40,6 +45,10 @@ def __init__(self, channel=None, can_filters=None, **config): :param dict config: Any backend dependent configurations are passed in this dictionary """ + pass + + def __str__(self): + return self.channel_info @abc.abstractmethod def recv(self, timeout=None): @@ -104,10 +113,9 @@ def __iter__(self): :yields: :class:`can.Message` msg objects. """ while True: - m = self.recv(timeout=1.0) - if m is not None: - yield m - logger.debug("done iterating over bus messages") + msg = self.recv(timeout=1.0) + if msg is not None: + yield msg def set_filters(self, can_filters=None): """Apply filtering to all messages received by this Bus. diff --git a/can/interface.py b/can/interface.py index 84758a448..174f03b58 100644 --- a/can/interface.py +++ b/can/interface.py @@ -4,6 +4,7 @@ import importlib from can.broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC +from pkg_resources import iter_entry_points from can.util import load_config # interface_name => (module, classname) @@ -18,12 +19,18 @@ 'nican': ('can.interfaces.nican', 'NicanBus'), 'iscan': ('can.interfaces.iscan', 'IscanBus'), 'virtual': ('can.interfaces.virtual', 'VirtualBus'), - 'neovi': ('can.interfaces.neovi_api', 'NeoVIBus'), + 'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'), 'vector': ('can.interfaces.vector', 'VectorBus'), 'slcan': ('can.interfaces.slcan', 'slcanBus') } +BACKENDS.update({ + interface.name: (interface.module_name, interface.attrs[0]) + for interface in iter_entry_points('python_can.interface') +}) + + class Bus(object): """ Instantiates a CAN Bus of the given `bustype`, falls back to reading a diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 942eb563a..1bd132731 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -2,7 +2,14 @@ """ Interfaces contain low level implementations that interact with CAN hardware. """ +from pkg_resources import iter_entry_points VALID_INTERFACES = set(['kvaser', 'serial', 'pcan', 'socketcan_native', 'socketcan_ctypes', 'socketcan', 'usb2can', 'ixxat', - 'nican', 'iscan', 'vector', 'virtual', 'neovi','slcan']) + 'nican', 'iscan', 'vector', 'virtual', 'neovi', + 'slcan']) + + +VALID_INTERFACES.update(set([ + interface.name for interface in iter_entry_points('python_can.interface') +])) diff --git a/can/interfaces/ics_neovi/__init__.py b/can/interfaces/ics_neovi/__init__.py new file mode 100644 index 000000000..5b1aa2052 --- /dev/null +++ b/can/interfaces/ics_neovi/__init__.py @@ -0,0 +1 @@ +from can.interfaces.ics_neovi.neovi_bus import NeoViBus diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py new file mode 100644 index 000000000..5e889091c --- /dev/null +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -0,0 +1,300 @@ +""" +ICS NeoVi interface module. + +python-ics is a Python wrapper around the API provided by Intrepid Control +Systems for communicating with their NeoVI range of devices. + +Implementation references: +* https://github.com/intrepidcs/python_ics +""" + +import logging +from collections import deque + +from can import Message, CanError +from can.bus import BusABC + +logger = logging.getLogger(__name__) + +try: + import ics +except ImportError as ie: + logger.warning( + "You won't be able to use the ICS NeoVi can backend without the " + "python-ics module installed!: %s", ie + ) + ics = None + + +class ICSApiError(CanError): + # A critical error which affects operation or accuracy. + ICS_SPY_ERR_CRITICAL = 0x10 + # An error which is not understood. + ICS_SPY_ERR_QUESTION = 0x20 + # An important error which may be critical depending on the application + ICS_SPY_ERR_EXCLAMATION = 0x30 + # An error which probably does not need attention. + ICS_SPY_ERR_INFORMATION = 0x40 + + def __init__( + self, error_number, description_short, description_long, + severity, restart_needed + ): + super(ICSApiError, self).__init__(description_short) + self.error_number = error_number + self.description_short = description_short + self.description_long = description_long + self.severity = severity + self.restart_needed = restart_needed == 1 + + def __str__(self): + return "{} {}".format(self.description_short, self.description_long) + + @property + def is_critical(self): + return self.severity == self.ICS_SPY_ERR_CRITICAL + + +class NeoViBus(BusABC): + """ + The CAN Bus implemented for the python_ics interface + https://github.com/intrepidcs/python_ics + """ + + def __init__(self, channel=None, can_filters=None, **config): + """ + + :param int channel: + The Channel id to create this bus with. + :param list can_filters: + A list of dictionaries each containing a "can_id" and a "can_mask". + :param use_system_timestamp: + Use system timestamp for can messages instead of the hardware time + stamp + + >>> [{"can_id": 0x11, "can_mask": 0x21}] + + """ + super(NeoViBus, self).__init__(channel, can_filters, **config) + if ics is None: + raise ImportError('Please install python-ics') + + 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) + ) + + # TODO: Add support for multiples channels + try: + channel = int(channel) + except ValueError: + raise ValueError('channel must be an integer') + + type_filter = config.get('type_filter') + serial = config.get('serial') + self.dev = self._open_device(type_filter, serial) + + self.channel_info = '%s %s CH:%s' % ( + self.dev.Name, + self.get_serial_number(self.dev), + channel + ) + logger.info("Using device: {}".format(self.channel_info)) + + ics.load_default_settings(self.dev) + + self.sw_filters = None + self.set_filters(can_filters) + self.rx_buffer = deque() + self.opened = True + + self.network = int(channel) if channel is not None else None + + # TODO: Change the scaling based on the device type + self.ts_scaling = ( + ics.NEOVI6_VCAN_TIMESTAMP_1, ics.NEOVI6_VCAN_TIMESTAMP_2 + ) + + @staticmethod + def get_serial_number(device): + """Decode (if needed) and return the ICS device serial string + + :param device: ics device + :return: ics device serial string + :rtype: str + """ + def to_base36(n, alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"): + return (to_base36(n // 36) + alphabet[n % 36]).lstrip("0") \ + if n > 0 else "0" + + a0000 = 604661760 + if device.SerialNumber >= a0000: + return to_base36(device.SerialNumber) + return str(device.SerialNumber) + + def shutdown(self): + super(NeoViBus, self).shutdown() + self.opened = False + ics.close_device(self.dev) + + def _open_device(self, type_filter=None, serial=None): + if type_filter is not None: + devices = ics.find_devices(type_filter) + else: + devices = ics.find_devices() + + for device in devices: + if serial is None or self.get_serial_number(device) == str(serial): + dev = device + break + else: + msg = ['No device'] + + if type_filter is not None: + msg.append('with type {}'.format(type_filter)) + if serial is not None: + msg.append('with serial {}'.format(serial)) + msg.append('found.') + raise Exception(' '.join(msg)) + ics.open_device(dev) + return dev + + def _process_msg_queue(self, timeout=None): + try: + messages, errors = ics.get_messages(self.dev, False, timeout) + except ics.RuntimeError: + return + for ics_msg in messages: + if ics_msg.NetworkID != self.network: + continue + if not self._is_filter_match(ics_msg.ArbIDOrHeader): + continue + self.rx_buffer.append(ics_msg) + if errors: + logger.warning("%d error(s) found" % errors) + + for msg in ics.get_error_messages(self.dev): + error = ICSApiError(*msg) + if error.is_critical: + raise error + logger.warning(error) + + def _is_filter_match(self, arb_id): + """ + If SW filtering is used, checks if the `arb_id` matches any of + the filters setup. + + :param int arb_id: + CAN ID to check against. + + :return: + True if `arb_id` matches any filters + (or if SW filtering is not used). + """ + if not self.sw_filters: + # Filtering done on HW or driver level or no filtering + return True + for can_filter in self.sw_filters: + if not (arb_id ^ can_filter['can_id']) & can_filter['can_mask']: + return True + return False + + def _get_timestamp_for_msg(self, ics_msg): + if self._use_system_timestamp: + # This is the system time stamp. + # TimeSystem is loaded with the value received from the timeGetTime + # call in the WIN32 multimedia API. + # + # The timeGetTime accuracy is up to 1 millisecond. See the WIN32 + # API documentation for more information. + # + # This timestamp is useful for time comparing with other system + # events or data which is not synced with the neoVI timestamp. + # + # Currently, TimeSystem2 is not used. + return ics_msg.TimeSystem + else: + # This is the hardware time stamp. + # The TimeStamp is reset to zero every time the OpenPort method is + # called. + return \ + float(ics_msg.TimeHardware2) * self.ts_scaling[1] + \ + float(ics_msg.TimeHardware) * self.ts_scaling[0] + + 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 + ) + + def recv(self, timeout=None): + msg = None + if not self.rx_buffer: + self._process_msg_queue(timeout=timeout) + + try: + ics_msg = self.rx_buffer.popleft() + msg = self._ics_msg_to_message(ics_msg) + except IndexError: + pass + return msg + + def send(self, msg, timeout=None): + if not self.opened: + return + data = tuple(msg.data) + + flags = 0 + if msg.is_extended_id: + flags |= ics.SPY_STATUS_XTD_FRAME + if msg.is_remote_frame: + flags |= ics.SPY_STATUS_REMOTE_FRAME + + message = ics.SpyMessage() + message.ArbIDOrHeader = msg.arbitration_id + message.NumberBytesData = len(data) + message.Data = data + message.StatusBitField = flags + message.StatusBitField2 = 0 + message.NetworkID = self.network + + try: + ics.transmit_messages(self.dev, message) + except ics.RuntimeError: + raise ICSApiError(*ics.get_last_api_error(self.dev)) + + def set_filters(self, can_filters=None): + """Apply filtering to all messages received by this Bus. + + Calling without passing any filters will reset the applied filters. + + :param list can_filters: + A list of dictionaries each containing a "can_id" and a "can_mask". + + >>> [{"can_id": 0x11, "can_mask": 0x21}] + + A filter matches, when + `` & can_mask == can_id & can_mask`` + + """ + self.sw_filters = can_filters or [] + + if not len(self.sw_filters): + logger.info("Filtering has been disabled") + else: + for can_filter in can_filters: + can_id = can_filter["can_id"] + can_mask = can_filter["can_mask"] + logger.info( + "Filtering on ID 0x%X, mask 0x%X", can_id, can_mask) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 03c697401..1b098200a 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -442,14 +442,13 @@ def recv(self, timeout=None): # The _message.dwTime is a 32bit tick value and will overrun, # so expect to see the value restarting from 0 rx_msg = Message( - self._message.dwTime / self._tick_resolution, # Relative time in s - True if self._message.uMsgInfo.Bits.rtr else False, - True if self._message.uMsgInfo.Bits.ext else False, - False, - self._message.dwMsgId, - self._message.uMsgInfo.Bits.dlc, - self._message.abData[:self._message.uMsgInfo.Bits.dlc], - self.channel + timestamp=self._message.dwTime / self._tick_resolution, # Relative time in s + is_remote_frame=True if self._message.uMsgInfo.Bits.rtr else False, + extended_id=True if self._message.uMsgInfo.Bits.ext else False, + arbitration_id=self._message.dwMsgId, + dlc=self._message.uMsgInfo.Bits.dlc, + data=self._message.abData[:self._message.uMsgInfo.Bits.dlc], + channel=self.channel ) log.debug('Recv()ed message %s', rx_msg) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 7da22addb..f430356e7 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -169,6 +169,12 @@ def __check_bus_handle_validity(handle, function, arguments): restype=canstat.c_canStatus, errcheck=__check_status) + canSetBusParamsFd = __get_canlib_function("canSetBusParamsFd", + argtypes=[c_canHandle, ctypes.c_long, + ctypes.c_uint, ctypes.c_uint, + ctypes.c_uint], + restype=canstat.c_canStatus, + errcheck=__check_status) canSetBusOutputControl = __get_canlib_function("canSetBusOutputControl", argtypes=[c_canHandle, @@ -254,15 +260,25 @@ def init_kvaser_library(): DRIVER_MODE_NORMAL = True -BITRATE_OBJS = {1000000 : canstat.canBITRATE_1M, - 500000 : canstat.canBITRATE_500K, - 250000 : canstat.canBITRATE_250K, - 125000 : canstat.canBITRATE_125K, - 100000 : canstat.canBITRATE_100K, - 83000 : canstat.canBITRATE_83K, - 62000 : canstat.canBITRATE_62K, - 50000 : canstat.canBITRATE_50K, - 10000 : canstat.canBITRATE_10K} +BITRATE_OBJS = { + 1000000: canstat.canBITRATE_1M, + 500000: canstat.canBITRATE_500K, + 250000: canstat.canBITRATE_250K, + 125000: canstat.canBITRATE_125K, + 100000: canstat.canBITRATE_100K, + 83000: canstat.canBITRATE_83K, + 62000: canstat.canBITRATE_62K, + 50000: canstat.canBITRATE_50K, + 10000: canstat.canBITRATE_10K +} + +BITRATE_FD = { + 500000: canstat.canFD_BITRATE_500K_80P, + 1000000: canstat.canFD_BITRATE_1M_80P, + 2000000: canstat.canFD_BITRATE_2M_80P, + 4000000: canstat.canFD_BITRATE_4M_80P, + 8000000: canstat.canFD_BITRATE_8M_60P +} class KvaserBus(BusABC): @@ -285,6 +301,8 @@ def __init__(self, channel, can_filters=None, **config): :param int bitrate: Bitrate of channel in bit/s + :param bool accept_virtual: + If virtual channels should be accepted. :param int tseg1: Time segment 1, that is, the number of quanta from (but not including) the Sync Segment to the sampling point. @@ -313,6 +331,11 @@ def __init__(self, channel, can_filters=None, **config): Only works if single_handle is also False. If you want to receive messages from other applications on the same computer, set this to True or set single_handle to True. + :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. """ log.info("CAN Filters: {}".format(can_filters)) log.info("Got configuration of: {}".format(config)) @@ -324,6 +347,9 @@ def __init__(self, channel, can_filters=None, **config): driver_mode = config.get('driver_mode', DRIVER_MODE_NORMAL) single_handle = config.get('single_handle', False) receive_own_messages = config.get('receive_own_messages', False) + accept_virtual = config.get('accept_virtual', True) + fd = config.get('fd', False) + data_bitrate = config.get('data_bitrate', None) try: channel = int(channel) @@ -331,9 +357,6 @@ def __init__(self, channel, can_filters=None, **config): raise ValueError('channel must be an integer') self.channel = channel - if 'tseg1' not in config and bitrate in BITRATE_OBJS: - bitrate = BITRATE_OBJS[bitrate] - log.debug('Initialising bus instance') self.single_handle = single_handle @@ -348,12 +371,33 @@ def __init__(self, channel, can_filters=None, **config): if idx == channel: self.channel_info = channel_info + flags = 0 + if accept_virtual: + flags |= canstat.canOPEN_ACCEPT_VIRTUAL + if fd: + flags |= canstat.canOPEN_CAN_FD + log.debug('Creating read handle to bus channel: %s' % channel) - self._read_handle = canOpenChannel(channel, canstat.canOPEN_ACCEPT_VIRTUAL) + self._read_handle = canOpenChannel(channel, flags) canIoCtl(self._read_handle, canstat.canIOCTL_SET_TIMER_SCALE, ctypes.byref(ctypes.c_long(TIMESTAMP_RESOLUTION)), 4) + + if fd: + if 'tseg1' not in config and bitrate in BITRATE_FD: + # Use predefined bitrate for arbitration + bitrate = BITRATE_FD[bitrate] + if data_bitrate in BITRATE_FD: + # Use predefined bitrate for data + data_bitrate = BITRATE_FD[data_bitrate] + elif not data_bitrate: + # Use same bitrate for arbitration and data phase + data_bitrate = bitrate + canSetBusParamsFd(self._read_handle, bitrate, tseg1, tseg2, sjw) + else: + if 'tseg1' not in config and bitrate in BITRATE_OBJS: + bitrate = BITRATE_OBJS[bitrate] canSetBusParams(self._read_handle, bitrate, tseg1, tseg2, sjw, no_samp, 0) # By default, use local echo if single handle is used (see #160) @@ -370,7 +414,7 @@ def __init__(self, channel, can_filters=None, **config): self._write_handle = self._read_handle else: log.debug('Creating separate handle for TX on channel: %s' % channel) - self._write_handle = canOpenChannel(channel, canstat.canOPEN_ACCEPT_VIRTUAL) + self._write_handle = canOpenChannel(channel, flags) canBusOn(self._read_handle) self.set_filters(can_filters) @@ -437,7 +481,7 @@ def recv(self, timeout=None): Read a message from kvaser device. """ arb_id = ctypes.c_long(0) - data = ctypes.create_string_buffer(8) + data = ctypes.create_string_buffer(64) dlc = ctypes.c_uint(0) flags = ctypes.c_uint(0) timestamp = ctypes.c_ulong(0) @@ -467,6 +511,9 @@ def recv(self, timeout=None): is_extended = bool(flags & canstat.canMSG_EXT) is_remote_frame = bool(flags & canstat.canMSG_RTR) is_error_frame = bool(flags & canstat.canMSG_ERROR_FRAME) + is_fd = bool(flags & canstat.canFDMSG_FDF) + bitrate_switch = bool(flags & canstat.canFDMSG_BRS) + error_state_indicator = bool(flags & canstat.canFDMSG_ESI) msg_timestamp = timestamp.value * TIMESTAMP_FACTOR rx_msg = Message(arbitration_id=arb_id.value, data=data_array[:dlc.value], @@ -474,6 +521,9 @@ def recv(self, timeout=None): extended_id=is_extended, is_error_frame=is_error_frame, is_remote_frame=is_remote_frame, + is_fd=is_fd, + bitrate_switch=bitrate_switch, + error_state_indicator=error_state_indicator, channel=self.channel, timestamp=msg_timestamp + self._timestamp_offset) rx_msg.flags = flags @@ -491,6 +541,10 @@ def send(self, msg, timeout=None): flags |= canstat.canMSG_RTR if msg.is_error_frame: flags |= canstat.canMSG_ERROR_FRAME + if msg.is_fd: + flags |= canstat.canFDMSG_FDF + if msg.bitrate_switch: + flags |= canstat.canFDMSG_BRS ArrayConstructor = ctypes.c_byte * msg.dlc buf = ArrayConstructor(*msg.data) canWrite(self._write_handle, @@ -520,9 +574,10 @@ def shutdown(self): # Wait for transmit queue to be cleared try: canWriteSync(self._write_handle, 100) - except CANLIBError as e: - log.warning("There may be messages in the transmit queue that could " - "not be transmitted before going bus off (%s)", e) + except CANLIBError: + # Not a huge deal and it seems that we get timeout if no messages + # exists in the buffer at all + pass if not self.single_handle: canBusOff(self._read_handle) canClose(self._read_handle) diff --git a/can/interfaces/kvaser/constants.py b/can/interfaces/kvaser/constants.py index b6a7dce8a..20ca5204e 100644 --- a/can/interfaces/kvaser/constants.py +++ b/can/interfaces/kvaser/constants.py @@ -61,6 +61,10 @@ def CANSTATUS_SUCCESS(status): canMSG_TXACK = 0x0040 canMSG_TXRQ = 0x0080 +canFDMSG_FDF = 0x010000 +canFDMSG_BRS = 0x020000 +canFDMSG_ESI = 0x040000 + canMSGERR_MASK = 0xff00 canMSGERR_HW_OVERRUN = 0x0200 canMSGERR_SW_OVERRUN = 0x0400 @@ -159,6 +163,8 @@ def CANSTATUS_SUCCESS(status): canOPEN_REQUIRE_INIT_ACCESS = 0x0080 canOPEN_NO_INIT_ACCESS = 0x0100 canOPEN_ACCEPT_LARGE_DLC = 0x0200 +canOPEN_CAN_FD = 0x0400 +canOPEN_CAN_FD_NONISO = 0x0800 canIOCTL_GET_RX_BUFFER_LEVEL = 8 canIOCTL_GET_TX_BUFFER_LEVEL = 9 @@ -230,3 +236,9 @@ def CANSTATUS_SUCCESS(status): canBITRATE_50K = -7 canBITRATE_83K = -8 canBITRATE_10K = -9 + +canFD_BITRATE_500K_80P = -1000 +canFD_BITRATE_1M_80P = -1001 +canFD_BITRATE_2M_80P = -1002 +canFD_BITRATE_4M_80P = -1003 +canFD_BITRATE_8M_60P = -1004 diff --git a/can/interfaces/neovi_api/__init__.py b/can/interfaces/neovi_api/__init__.py deleted file mode 100644 index 9a9e7ff02..000000000 --- a/can/interfaces/neovi_api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from can.interfaces.neovi_api.neovi_api import NeoVIBus diff --git a/can/interfaces/neovi_api/neovi_api.py b/can/interfaces/neovi_api/neovi_api.py deleted file mode 100644 index 4a8aa2044..000000000 --- a/can/interfaces/neovi_api/neovi_api.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -pyneovi interface module. - -pyneovi is a Python wrapper around the API provided by Intrepid Control Systems -for communicating with their NeoVI range of devices. - -Implementation references: -* http://pyneovi.readthedocs.io/en/latest/ -* https://bitbucket.org/Kemp_J/pyneovi -""" - -import logging - -logger = logging.getLogger(__name__) - -try: - import queue -except ImportError: - import Queue as queue - -try: - from neovi import neodevice - from neovi import neovi - from neovi.structures import icsSpyMessage -except ImportError as e: - logger.warning("Cannot load pyneovi: %s", e) - -from can import Message -from can.bus import BusABC - - -SPY_STATUS_XTD_FRAME = 0x04 -SPY_STATUS_REMOTE_FRAME = 0x08 - - -def neo_device_name(device_type): - names = { - neovi.NEODEVICE_BLUE: 'neoVI BLUE', - neovi.NEODEVICE_DW_VCAN: 'ValueCAN', - neovi.NEODEVICE_FIRE: 'neoVI FIRE', - neovi.NEODEVICE_VCAN3: 'ValueCAN3', - neovi.NEODEVICE_YELLOW: 'neoVI YELLOW', - neovi.NEODEVICE_RED: 'neoVI RED', - neovi.NEODEVICE_ECU: 'neoECU', - # neovi.NEODEVICE_IEVB: '' - } - return names.get(device_type, 'Unknown neoVI') - - -class NeoVIBus(BusABC): - """ - The CAN Bus implemented for the pyneovi interface. - """ - - def __init__(self, channel=None, can_filters=None, **config): - """ - - :param int channel: - The Channel id to create this bus with. - """ - type_filter = config.get('type_filter', neovi.NEODEVICE_ALL) - neodevice.init_api() - self.device = neodevice.find_devices(type_filter)[0] - self.device.open() - self.channel_info = '%s %s on channel %s' % ( - neo_device_name(self.device.get_type()), - self.device.device.SerialNumber, - channel - ) - - self.rx_buffer = queue.Queue() - - self.network = int(channel) if channel is not None else None - self.device.subscribe_to(self._rx_buffer, network=self.network) - - def __del__(self): - self.shutdown() - - def shutdown(self): - self.device.pump_messages = False - if self.device.msg_queue_thread is not None: - self.device.msg_queue_thread.join() - - def _rx_buffer(self, msg, user_data): - self.rx_buffer.put_nowait(msg) - - def _ics_msg_to_message(self, ics_msg): - return Message( - timestamp=neovi.GetTimeStampForMsg(self.device.handle, ics_msg)[1], - arbitration_id=ics_msg.ArbIDOrHeader, - data=ics_msg.Data[:ics_msg.NumberBytesData], - dlc=ics_msg.NumberBytesData, - extended_id=bool(ics_msg.StatusBitField & - SPY_STATUS_XTD_FRAME), - is_remote_frame=bool(ics_msg.StatusBitField & - SPY_STATUS_REMOTE_FRAME), - ) - - def recv(self, timeout=None): - try: - ics_msg = self.rx_buffer.get(block=True, timeout=timeout) - except queue.Empty: - pass - else: - if ics_msg.NetworkID == self.network: - return self._ics_msg_to_message(ics_msg) - - def send(self, msg, timeout=None): - data = tuple(msg.data) - flags = SPY_STATUS_XTD_FRAME if msg.is_extended_id else 0 - if msg.is_remote_frame: - flags |= SPY_STATUS_REMOTE_FRAME - - ics_msg = icsSpyMessage() - ics_msg.ArbIDOrHeader = msg.arbitration_id - ics_msg.NumberBytesData = len(data) - ics_msg.Data = data - ics_msg.StatusBitField = flags - ics_msg.StatusBitField2 = 0 - ics_msg.DescriptionID = self.device.tx_id - self.device.tx_id += 1 - self.device.tx_raw_message(ics_msg, self.network) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index eb9f6c792..8ff8b162c 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -130,13 +130,13 @@ def bits(n): if stsReturn[0] != PCAN_ERROR_OK: text = "An error occurred. Error-code's text ({0:X}h) couldn't be retrieved".format(error) else: - text = stsReturn[1].decode('utf-8') + text = stsReturn[1].decode('utf-8', errors='replace') strings.append(text) complete_text = '\n'.join(strings) else: - complete_text = stsReturn[1].decode('utf-8') + complete_text = stsReturn[1].decode('utf-8', errors='replace') return complete_text diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 0b5c00fcb..4f8499028 100755 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -1,84 +1,88 @@ """ Interface for slcan compatible interfaces (win32/linux). -(Linux could use slcand/socketcan also). + +Note Linux users can use slcand/socketcan as well. """ + from __future__ import absolute_import -import serial import io import time import logging -from can import CanError, BusABC, Message +import serial +from can import BusABC, Message logger = logging.getLogger(__name__) -class slcanBus(BusABC): - """slcan interface""" - def write(self, str): - if not str.endswith("\r"): - str += "\r" - self.serialPort.write(str.decode()) +class slcanBus(BusABC): + """ + slcan interface + """ + + # the supported bitrates and their commands + _BITRATES = { + 10000: 'S0', + 20000: 'S1', + 50000: 'S2', + 100000: 'S3', + 125000: 'S4', + 250000: 'S5', + 500000: 'S6', + 750000: 'S7', + 1000000: 'S8', + 83300: 'S9' + } + + _SLEEP_AFTER_SERIAL_OPEN = 2 # in seconds + + def write(self, string): + if not string.endswith('\r'): + string += '\r' + self.serialPort.write(string.decode()) self.serialPort.flush() def open(self): - self.write("O") + self.write('O') def close(self): - self.write("C") + self.write('C') - - def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None , **kwargs): + def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwargs): """ :param string channel: port of underlying serial or usb device (e.g. /dev/ttyUSB0, COM8, ...) + Must not be empty. :param int ttyBaudrate: baudrate of underlying serial or usb device :param int bitrate: Bitrate in bits/s :param float poll_interval: Poll interval in seconds when reading messages - :param float timeout + :param float timeout: timeout in seconds when reading message """ - - if channel == '': + 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) + newline='\r', line_buffering=True) + + time.sleep(self._SLEEP_AFTER_SERIAL_OPEN) - time.sleep(2) if bitrate is not None: self.close() - if bitrate == 10000: - self.write('S0') - elif bitrate == 20000: - self.write('S1') - elif bitrate == 50000: - self.write('S2') - elif bitrate == 100000: - self.write('S3') - elif bitrate == 125000: - self.write('S4') - elif bitrate == 250000: - self.write('S5') - elif bitrate == 500000: - self.write('S6') - elif bitrate == 750000: - self.write('S7') - elif bitrate == 1000000: - self.write('S8') - elif bitrate == 83300: - self.write('S9') + if bitrate in self._BITRATES: + self.write(self._BITRATES[bitrate]) else: - raise ValueError("Invalid bitrate, choose one of 10000 20000 50000 100000 125000 250000 500000 750000 1000000 83300") + raise ValueError("Invalid bitrate, choose one of " + (', '.join(self._BITRATES)) + '.') self.open() super(slcanBus, self).__init__(channel, **kwargs) @@ -89,27 +93,31 @@ def recv(self, timeout=None): canId = None remote = False + extended = False frame = [] readStr = self.serialPort.readline() - if readStr is None or len(readStr) == 0: + if not readStr: return None else: - if readStr[0] == 'T': # entended frame + if readStr[0] == 'T': + # extended frame canId = int(readStr[1:9], 16) dlc = int(readStr[9]) extended = True for i in range(0, dlc): frame.append(int(readStr[10 + i * 2:12 + i * 2], 16)) - elif readStr[0] == 't': # normal frame + elif readStr[0] == 't': + # normal frame canId = int(readStr[1:4], 16) dlc = int(readStr[4]) for i in range(0, dlc): frame.append(int(readStr[5 + i * 2:7 + i * 2], 16)) - extended = False - elif readStr[0] == 'r': # remote frame + elif readStr[0] == 'r': + # remote frame canId = int(readStr[1:4], 16) remote = True - elif readStr[0] == 'R': # remote extended frame + elif readStr[0] == 'R': + # remote extended frame canId = int(readStr[1:9], 16) extended = True remote = True @@ -140,6 +148,5 @@ def send(self, msg, timeout=None): sendStr += "%02X" % msg.data[i] self.write(sendStr) - def shutdown(self): - self.close() \ No newline at end of file + self.close() diff --git a/can/interfaces/socketcan/socketcan_constants.py b/can/interfaces/socketcan/socketcan_constants.py index a1b92e148..b3a7447fa 100644 --- a/can/interfaces/socketcan/socketcan_constants.py +++ b/can/interfaces/socketcan/socketcan_constants.py @@ -30,6 +30,7 @@ RX_ANNOUNCE_RESUME = 0x0100 TX_RESET_MULTI_IDX = 0x0200 RX_RTR_FRAME = 0x0400 +CAN_FD_FRAME = 0x0800 CAN_RAW = 1 CAN_BCM = 2 @@ -58,6 +59,11 @@ SKT_ERRFLG = 0x0001 SKT_RTRFLG = 0x0002 +CANFD_BRS = 0x01 +CANFD_ESI = 0x02 + +CANFD_MTU = 72 + PYCAN_ERRFLG = 0x0020 PYCAN_STDFLG = 0x0002 PYCAN_RTRFLG = 0x0001 diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py index d1f64384d..8d9b2e60d 100644 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ b/can/interfaces/socketcan/socketcan_ctypes.py @@ -125,12 +125,16 @@ def recv(self, timeout=None): def send(self, msg, timeout=None): frame = _build_can_frame(msg) + if timeout: # Wait for write availability. write will fail below on timeout - select.select([], [self.socket], [], timeout) + _, ready_send_sockets, _ = select.select([], [self.socket], [], timeout) + if not ready_send_sockets: + raise can.CanError("Timeout while sending") + bytes_sent = libc.write(self.socket, ctypes.byref(frame), ctypes.sizeof(frame)) + if bytes_sent == -1: - log.debug("Error sending frame :-/") raise can.CanError("can.socketcan.ctypes failed to transmit") elif bytes_sent == 0: raise can.CanError("Transmit buffer overflow") @@ -346,9 +350,9 @@ def _build_can_frame(message): # TODO need to understand the extended frame format frame = CAN_FRAME() frame.can_id = arbitration_id - frame.can_dlc = len(message.data) + frame.can_dlc = message.dlc - frame.data[0:frame.can_dlc] = message.data + frame.data[0:len(message.data)] = message.data log.debug("sizeof frame: %d", ctypes.sizeof(frame)) return frame diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 1c4280c18..10933d79e 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -44,11 +44,10 @@ # The 32bit can id is directly followed by the 8bit data link count # The data field is aligned on an 8 byte boundary, hence we add padding # which aligns the data field to an 8 byte boundary. -can_frame_fmt = "=IB3x8s" -can_frame_size = struct.calcsize(can_frame_fmt) +CAN_FRAME_HEADER_STRUCT = struct.Struct("=IBB2x") -def build_can_frame(can_id, data): +def build_can_frame(msg): """ CAN frame packing/unpacking (see 'struct can_frame' in ) /** * struct can_frame - basic CAN frame structure @@ -61,10 +60,34 @@ def build_can_frame(can_id, data): __u8 can_dlc; /* data length code: 0 .. 8 */ __u8 data[8] __attribute__((aligned(8))); }; + + /** + * struct canfd_frame - CAN flexible data rate frame structure + * @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition + * @len: frame payload length in byte (0 .. CANFD_MAX_DLEN) + * @flags: additional flags for CAN FD + * @__res0: reserved / padding + * @__res1: reserved / padding + * @data: CAN FD frame payload (up to CANFD_MAX_DLEN byte) + */ + struct canfd_frame { + canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */ + __u8 len; /* frame payload length in byte */ + __u8 flags; /* additional flags for CAN FD */ + __u8 __res0; /* reserved / padding */ + __u8 __res1; /* reserved / padding */ + __u8 data[CANFD_MAX_DLEN] __attribute__((aligned(8))); + }; """ - can_dlc = len(data) - data = data.ljust(8, b'\x00') - return struct.pack(can_frame_fmt, can_id, can_dlc, data) + can_id = _add_flags_to_can_id(msg) + flags = 0 + if msg.bitrate_switch: + flags |= CANFD_BRS + if msg.error_state_indicator: + flags |= CANFD_ESI + max_len = 64 if msg.is_fd else 8 + data = msg.data.ljust(max_len, b'\x00') + return CAN_FRAME_HEADER_STRUCT.pack(can_id, msg.dlc, flags) + data def build_bcm_header(opcode, flags, count, ival1_seconds, ival1_usec, ival2_seconds, ival2_usec, can_id, nframes): @@ -90,15 +113,16 @@ def build_bcm_header(opcode, flags, count, ival1_seconds, ival1_usec, ival2_seco nframes) -def build_bcm_tx_delete_header(can_id): +def build_bcm_tx_delete_header(can_id, flags): opcode = CAN_BCM_TX_DELETE - return build_bcm_header(opcode, 0, 0, 0, 0, 0, 0, can_id, 1) + return build_bcm_header(opcode, flags, 0, 0, 0, 0, 0, can_id, 1) -def build_bcm_transmit_header(can_id, count, initial_period, subsequent_period): +def build_bcm_transmit_header(can_id, count, initial_period, subsequent_period, + msg_flags): opcode = CAN_BCM_TX_SETUP - flags = SETTIMER | STARTTIMER + flags = msg_flags | SETTIMER | STARTTIMER if initial_period > 0: # Note `TX_COUNTEVT` creates the message TX_EXPIRED when count expires @@ -118,8 +142,11 @@ def split_time(value): def dissect_can_frame(frame): - can_id, can_dlc, data = struct.unpack(can_frame_fmt, frame) - return can_id, can_dlc, data[:can_dlc] + can_id, can_dlc, flags = CAN_FRAME_HEADER_STRUCT.unpack_from(frame) + if len(frame) != CANFD_MTU: + # Flags not valid in non-FD frames + flags = 0 + return can_id, can_dlc, flags, frame[8:8+can_dlc] def create_bcm_socket(channel): @@ -136,7 +163,7 @@ def create_bcm_socket(channel): return s -def send_bcm(socket, data): +def send_bcm(bcm_socket, data): """ Send raw frame to a BCM socket and handle errors. @@ -145,21 +172,21 @@ def send_bcm(socket, data): :return: """ try: - return socket.send(data) + return bcm_socket.send(data) except OSError as e: base = "Couldn't send CAN BCM frame. OS Error {}: {}\n".format(e.errno, os.strerror(e.errno)) if e.errno == errno.EINVAL: - raise can.CanError( - base + "You are probably referring to a non-existing frame.") + raise can.CanError(base + "You are probably referring to a non-existing frame.") + elif e.errno == errno.ENETDOWN: - raise can.CanError( - base + "The CAN interface appears to be down." - ) + raise can.CanError(base + "The CAN interface appears to be down.") + elif e.errno == errno.EBADF: raise can.CanError(base + "The CAN socket appears to be closed.") + else: - raise + raise e def _add_flags_to_can_id(message): can_id = message.arbitration_id @@ -184,7 +211,8 @@ def __init__(self, channel, *args, **kwargs): super(SocketCanBCMBase, self).__init__(*args, **kwargs) -class CyclicSendTask(SocketCanBCMBase, LimitedDurationCyclicSendTaskABC, ModifiableCyclicTaskABC, RestartableCyclicTaskABC): +class CyclicSendTask(SocketCanBCMBase, LimitedDurationCyclicSendTaskABC, + ModifiableCyclicTaskABC, RestartableCyclicTaskABC): """ A socketcan cyclic send task supports: @@ -196,7 +224,6 @@ class CyclicSendTask(SocketCanBCMBase, LimitedDurationCyclicSendTaskABC, Modifia def __init__(self, channel, message, period): """ - :param channel: The name of the CAN channel to connect to. :param message: The message to be sent periodically. :param period: The rate in seconds at which to send the message. @@ -208,8 +235,10 @@ def __init__(self, channel, message, period): def _tx_setup(self, message): # Create a low level packed frame to pass to the kernel self.can_id_with_flags = _add_flags_to_can_id(message) - header = build_bcm_transmit_header(self.can_id_with_flags, 0, 0.0, self.period) - frame = build_can_frame(self.can_id_with_flags, message.data) + self.flags = CAN_FD_FRAME if message.is_fd else 0 + header = build_bcm_transmit_header(self.can_id_with_flags, 0, 0.0, + self.period, self.flags) + frame = build_can_frame(message) log.debug("Sending BCM command") send_bcm(self.bcm_socket, header + frame) @@ -222,7 +251,7 @@ def stop(self): """ log.debug("Stopping periodic task") - stopframe = build_bcm_tx_delete_header(self.can_id_with_flags) + stopframe = build_bcm_tx_delete_header(self.can_id_with_flags, self.flags) send_bcm(self.bcm_socket, stopframe) def modify_data(self, message): @@ -248,18 +277,19 @@ def __init__(self, channel, message, count, initial_period, subsequent_period): super(MultiRateCyclicSendTask, self).__init__(channel, message, subsequent_period) # Create a low level packed frame to pass to the kernel - frame = build_can_frame(self.can_id, message.data) + frame = build_can_frame(message) header = build_bcm_transmit_header( - self.can_id, + self.can_id_with_flags, count, initial_period, - subsequent_period) + subsequent_period, + self.flags) log.info("Sending BCM TX_SETUP command") send_bcm(self.bcm_socket, header + frame) -def createSocket(can_protocol=None): +def create_socket(can_protocol=None): """Creates a CAN socket. The socket can be BCM or RAW. The socket will be returned unbound to any interface. @@ -286,7 +316,7 @@ def createSocket(can_protocol=None): return sock -def bindSocket(sock, channel='can0'): +def bind_socket(sock, channel='can0'): """ Binds the given socket to the given interface. @@ -300,7 +330,7 @@ def bindSocket(sock, channel='can0'): log.debug('Bound socket.') -def captureMessage(sock): +def capture_message(sock): """ Captures a message from given socket. @@ -311,7 +341,7 @@ def captureMessage(sock): """ # Fetching the Arb ID, DLC and Data try: - cf, addr = sock.recvfrom(can_frame_size) + cf, addr = sock.recvfrom(CANFD_MTU) except BlockingIOError: log.debug('Captured no data, socket in non-blocking mode.') return None @@ -323,7 +353,8 @@ def captureMessage(sock): log.exception("Captured no data.") return None - can_id, can_dlc, data = dissect_can_frame(cf) + 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) # Fetching the timestamp binary_structure = "@LL" @@ -340,6 +371,9 @@ def captureMessage(sock): is_extended_frame_format = bool(can_id & 0x80000000) is_remote_transmission_request = bool(can_id & 0x40000000) is_error_frame = bool(can_id & 0x20000000) + is_fd = len(cf) == CANFD_MTU + bitrate_switch = bool(flags & CANFD_BRS) + error_state_indicator = bool(flags & CANFD_ESI) if is_extended_frame_format: log.debug("CAN: Extended") @@ -354,6 +388,9 @@ def captureMessage(sock): extended_id=is_extended_frame_format, is_remote_frame=is_remote_transmission_request, is_error_frame=is_error_frame, + is_fd=is_fd, + bitrate_switch=bitrate_switch, + error_state_indicator=error_state_indicator, dlc=can_dlc, data=data) @@ -363,74 +400,81 @@ def captureMessage(sock): class SocketcanNative_Bus(BusABC): - channel_info = "native socketcan channel" - def __init__(self, channel, receive_own_messages=False, **kwargs): + def __init__(self, channel, receive_own_messages=False, fd=False, **kwargs): """ :param str channel: The can interface name with which to create this bus. An example channel would be 'vcan0'. :param bool receive_own_messages: If messages transmitted should also be received back. + :param bool fd: + If CAN-FD frames should be supported. :param list can_filters: A list of dictionaries, each containing a "can_id" and a "can_mask". """ - self.socket = createSocket(CAN_RAW) + self.socket = create_socket(CAN_RAW) self.channel = channel + self.channel_info = "native socketcan channel '%s'" % channel - # Add any socket options such as can frame filters - if 'can_filters' in kwargs and len(kwargs['can_filters']) > 0: + # add any socket options such as can frame filters + if 'can_filters' in kwargs and kwargs['can_filters']: # = not None or empty log.debug("Creating a filtered can bus") self.set_filters(kwargs['can_filters']) + + # set the receive_own_messages paramater try: self.socket.setsockopt(socket.SOL_CAN_RAW, socket.CAN_RAW_RECV_OWN_MSGS, struct.pack('i', receive_own_messages)) - except Exception as e: + except socket.error as e: log.error("Could not receive own messages (%s)", e) - bindSocket(self.socket, channel) + if fd: + self.socket.setsockopt(socket.SOL_CAN_RAW, + socket.CAN_RAW_FD_FRAMES, + struct.pack('i', 1)) + + bind_socket(self.socket, channel) super(SocketcanNative_Bus, self).__init__() def shutdown(self): self.socket.close() def recv(self, timeout=None): - data_ready = True try: if timeout is not None: - data_ready = len(select.select([self.socket], [], [], timeout)[0]) > 0 + # get all sockets that are ready (can be a list with a single value + # being self.socket or an empty list if self.socket is not ready) + ready_receive_sockets, _, _ = select.select([self.socket], [], [], timeout) + else: + ready_receive_sockets = True except OSError: # something bad happened (e.g. the interface went down) log.exception("Error while waiting for timeout") return None - if data_ready: - return captureMessage(self.socket) + if ready_receive_sockets: # not empty + return capture_message(self.socket) else: # socket wasn't readable or timeout occurred return None def send(self, msg, timeout=None): log.debug("We've been asked to write a message to the bus") - arbitration_id = msg.arbitration_id - if msg.id_type: - log.debug("sending an extended id type message") - arbitration_id |= 0x80000000 - if msg.is_remote_frame: - log.debug("requesting a remote frame") - arbitration_id |= 0x40000000 - if msg.is_error_frame: - log.warning("Trying to send an error frame - this won't work") - arbitration_id |= 0x20000000 - log_tx.debug("Sending: %s", msg) + logger_tx = log.getChild("tx") + logger_tx.debug("sending: %s", msg) if timeout: - # Wait for write availability. send will fail below on timeout - select.select([], [self.socket], [], timeout) + # Wait for write availability + _, ready_send_sockets, _ = select.select([], [self.socket], [], timeout) + if not ready_send_sockets: + raise can.CanError("Timeout while sending") + try: - bytes_sent = self.socket.send(build_can_frame(arbitration_id, msg.data)) + bytes_sent = self.socket.send(build_can_frame(msg)) except OSError as exc: raise can.CanError("Transmit failed (%s)" % exc) + if bytes_sent == 0: raise can.CanError("Transmit buffer overflow") @@ -447,8 +491,7 @@ def set_filters(self, can_filters=None): filter_struct = pack_filters(can_filters) self.socket.setsockopt(socket.SOL_CAN_RAW, socket.CAN_RAW_FILTER, - filter_struct - ) + filter_struct) if __name__ == "__main__": @@ -461,17 +504,17 @@ def set_filters(self, can_filters=None): # ifconfig vcan0 up log.setLevel(logging.DEBUG) - def receiver(e): - receiver_socket = createSocket() - bindSocket(receiver_socket, 'vcan0') + def receiver(event): + receiver_socket = create_socket() + bind_socket(receiver_socket, 'vcan0') print("Receiver is waiting for a message...") - e.set() - print("Receiver got: ", captureMessage(receiver_socket)) + event.set() + print("Receiver got: ", capture_message(receiver_socket)) - def sender(e): - e.wait() - sender_socket = createSocket() - bindSocket(sender_socket, 'vcan0') + def sender(event): + event.wait() + sender_socket = create_socket() + bind_socket(sender_socket, 'vcan0') sender_socket.send(build_can_frame(0x01, b'\x01\x02\x03')) print("Sender sent a message.") diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index eb18d6e39..19e2c78da 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -174,3 +174,9 @@ def shutdown(self): vxlapi.xlDeactivateChannel(self.port_handle, self.mask) vxlapi.xlClosePort(self.port_handle) vxlapi.xlCloseDriver() + + def reset(self): + vxlapi.xlDeactivateChannel(self.port_handle, self.mask) + vxlapi.xlActivateChannel(self.port_handle, self.mask, + vxlapi.XL_BUS_TYPE_CAN, 0) + diff --git a/can/io/__init__.py b/can/io/__init__.py index 4273abcde..fd2738567 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -9,5 +9,5 @@ from .asc import ASCWriter, ASCReader from .blf import BLFReader, BLFWriter from .csv import CSVWriter -from .sqlite import SqlReader, SqliteWriter +from .sqlite import SqliteReader, SqliteWriter from .stdout import Printer diff --git a/can/io/asc.py b/can/io/asc.py index 6b800d44b..d69436cf5 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -1,61 +1,89 @@ +from datetime import datetime +import time +import logging + from can.listener import Listener from can.message import Message -from datetime import datetime -import time CAN_MSG_EXT = 0x80000000 CAN_ID_MASK = 0x1FFFFFFF +logger = logging.getLogger('can.io.asc') + + class ASCReader(object): """ Iterator of CAN messages from a ASC Logging File. """ def __init__(self, filename): - self.fp = open(filename, "r") + self.file = open(filename, 'r') + + @staticmethod + def _extract_can_id(str_can_id): + if str_can_id[-1:].lower() == "x": + is_extended = True + can_id = int(str_can_id[0:-1], 16) + 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) def __iter__(self): - def extractCanId(strCanId): - if strCanId[-1:].lower() == "x": - isExtended = True - can_id = int(strCanId[0:-1], 16) - else: - isExtended = False - can_id = int(strCanId, 16) - return (can_id, isExtended) + for line in self.file: + logger.debug("ASCReader: parsing line: '%s'", line.splitlines()[0]) - for line in self.fp: temp = line.strip() - if len(temp) == 0 or not temp[0].isdigit(): + if not temp or not temp[0].isdigit(): continue - (time, channel, dummy) = temp.split(None,2) # , frameType, dlc, frameData - time = float(time) - if dummy.strip()[0:10] == "ErrorFrame": - time = float(time) - msg = Message(timestamp=time, is_error_frame=True) - yield msg - continue - if not channel.isdigit() or dummy.strip()[0:10] == "Statistic:": + try: + (timestamp, channel, dummy) = temp.split(None, 2) # , frameType, dlc, frameData + except ValueError: + # we parsed an empty comment continue - if dummy[-1:].lower() == "r": - (canId, _) = dummy.split(None, 1) - msg = Message(timestamp=time, - arbitration_id=extractCanId(canId)[0] & CAN_ID_MASK, - extended_id=extractCanId(canId)[1], + + timestamp = float(timestamp) + + if dummy.strip()[0:10] == 'ErrorFrame': + msg = Message(timestamp=timestamp, is_error_frame=True) + yield msg + + elif not channel.isdigit() or dummy.strip()[0:10] == 'Statistic:': + 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) + msg = Message(timestamp=timestamp, + arbitration_id=can_id_num & CAN_ID_MASK, + extended_id=is_extended_id, is_remote_frame=True) yield msg + else: - (canId, direction,_,dlc,data) = dummy.split(None,4) + try: + # this only works if dlc > 0 and thus data is availabe + (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) + # and we set data to an empty sequence manually + data = '' dlc = int(dlc) frame = bytearray() data = data.split() for byte in data[0:dlc]: - frame.append(int(byte,16)) - msg = Message(timestamp=time, - arbitration_id=extractCanId(canId)[0] & CAN_ID_MASK, - extended_id=extractCanId(canId)[1], + frame.append(int(byte, 16)) + + (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) @@ -65,14 +93,14 @@ def extractCanId(strCanId): class ASCWriter(Listener): """Logs CAN data to an ASCII log file (.asc)""" - LOG_STRING = "{time: 9.4f} {channel} {id:<15} Rx {dtype} {data}\n" - EVENT_STRING = "{time: 9.4f} {message}\n" + LOG_STRING = "{time: 9.4f} {channel} {id:<15} Rx {dtype} {data}\n" + EVENT_STRING = "{time: 9.4f} {message}\n" def __init__(self, filename, channel=1): now = datetime.now().strftime("%a %b %m %I:%M:%S %p %Y") self.channel = channel self.started = time.time() - self.log_file = open(filename, "w") + self.log_file = open(filename, 'w') 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") @@ -87,6 +115,11 @@ def stop(self): def log_event(self, message, timestamp=None): """Add an arbitrary message to the log file.""" + + if not message: # if empty or None + logger.debug("ASCWriter: ignoring empty message") + return + timestamp = (timestamp or time.time()) if timestamp >= self.started: timestamp -= self.started @@ -97,7 +130,7 @@ def log_event(self, message, timestamp=None): def on_message_received(self, msg): if msg.is_error_frame: - self.log_event("{} ErrorFrame".format(self.channel), msg.timestamp) + self.log_event("{} ErrorFrame".format(self.channel), msg.timestamp) return if msg.is_remote_frame: diff --git a/can/io/blf.py b/can/io/blf.py index 10355efb1..bdd994d17 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -10,6 +10,7 @@ of uncompressed data each. This data contains the actual CAN messages and other objects types. """ + import struct import zlib import datetime @@ -37,6 +38,10 @@ # channel, flags, dlc, arbitration id, data CAN_MSG_STRUCT = struct.Struct(" self.MAX_TIME_BETWEEN_WRITES: log.debug("Max timeout between writes reached") break - m = self.get_message(self.GET_MESSAGE_TIMEOUT) + msg = self.get_message(self.GET_MESSAGE_TIMEOUT) - if len(messages) > 0: + count = len(messages) + if count > 0: with self.conn: - log.debug("Writing %s frames to db", len(messages)) - self.conn.executemany(SqliteWriter.insert_msg_template, messages) - num_frames += len(messages) + 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) @@ -143,4 +178,3 @@ def stop(self): self.stop_running_event.set() log.debug("Stopping sqlite writer") self.writer_thread.join() - diff --git a/can/message.py b/can/message.py index 8a6765530..4f02476dd 100644 --- a/can/message.py +++ b/can/message.py @@ -4,13 +4,27 @@ class Message(object): """ - The :class:`~can.Message` object is used to represent CAN messages for both sending and receiving. + The :class:`~can.Message` object is used to represent CAN messages for + both sending and receiving. + + 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. + + .. note:: + + 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. - Messages can use extended identifiers, be remote or error frames, and contain data. """ def __init__(self, timestamp=0.0, is_remote_frame=False, extended_id=True, is_error_frame=False, arbitration_id=0, dlc=None, data=None, + is_fd=False, bitrate_switch=False, error_state_indicator=False, channel=None): self.timestamp = timestamp @@ -22,6 +36,10 @@ def __init__(self, timestamp=0.0, is_remote_frame=False, extended_id=True, self.arbitration_id = arbitration_id self.channel = channel + self.is_fd = is_fd + self.bitrate_switch = bitrate_switch + self.error_state_indicator = error_state_indicator + if data is None or is_remote_frame: self.data = bytearray() elif isinstance(data, bytearray): @@ -38,7 +56,10 @@ def __init__(self, timestamp=0.0, is_remote_frame=False, extended_id=True, else: self.dlc = dlc - assert self.dlc <= 8, "data link count was {} but it must be less than or equal to 8".format(self.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) def __str__(self): field_strings = ["Timestamp: {0:15.6f}".format(self.timestamp)] @@ -53,6 +74,7 @@ def __str__(self): "X" if self.id_type else "S", "E" if self.is_error_frame else " ", "R" if self.is_remote_frame else " ", + "F" if self.is_fd else " ", ]) field_strings.append(flag_string) @@ -62,7 +84,7 @@ def __str__(self): 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 len(data_strings) > 0: + if data_strings: # if not empty field_strings.append(" ".join(data_strings).ljust(24, " ")) else: field_strings.append(" " * 24) @@ -70,7 +92,7 @@ def __str__(self): if (self.data is not None) and (self.data.isalnum()): try: field_strings.append("'{}'".format(self.data.decode('utf-8'))) - except UnicodeError as e: + except UnicodeError: pass return " ".join(field_strings).strip() @@ -95,14 +117,36 @@ def __repr__(self): "data=[{}]".format(", ".join(data))] if self.channel is not None: args.append("channel={}".format(self.channel)) + 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 self.arbitration_id == other.arbitration_id and - #self.timestamp == other.timestamp and + #self.timestamp == other.timestamp and # allow the timestamp to differ self.id_type == other.id_type 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) + self.is_error_frame == other.is_error_frame 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__() diff --git a/can/notifier.py b/can/notifier.py index bc9f8f68c..4c2c59604 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -1,4 +1,7 @@ import threading +import logging + +logger = logging.getLogger('can.Notifier') class Notifier(object): @@ -14,15 +17,15 @@ def __init__(self, bus, listeners, timeout=None): self.listeners = listeners self.bus = bus self.timeout = timeout - #: Exception raised in thread + + # exception raised in thread self.exception = None self.running = threading.Event() self.running.set() - self._reader = threading.Thread(target=self.rx_thread) + self._reader = threading.Thread(target=self.rx_thread, name="can.notifier") self._reader.daemon = True - self._reader.start() def stop(self): diff --git a/can/util.py b/can/util.py index 66c468f41..dd1bf67a1 100644 --- a/can/util.py +++ b/can/util.py @@ -156,7 +156,7 @@ def load_config(path=None, config=None): system_config['interface'] = choose_socketcan_implementation() if system_config['interface'] not in VALID_INTERFACES: - raise NotImplementedError('Invalid CAN Bus Type - {}'.format(can.rc['interface'])) + raise NotImplementedError('Invalid CAN Bus Type - {}'.format(system_config['interface'])) if 'bitrate' in system_config: system_config['bitrate'] = int(system_config['bitrate']) diff --git a/doc/conf.py b/doc/conf.py index 5d9bc7f05..f56298c53 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -64,10 +64,9 @@ # The master toctree document. master_doc = 'index' - # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +language = 'en' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: @@ -131,14 +130,14 @@ #html_logo = None # The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +#html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. diff --git a/doc/development.rst b/doc/development.rst index fe6eca046..f7e09b671 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -8,6 +8,21 @@ Contributing Contribute to source code, documentation, examples and report issues: https://github.com/hardbyte/python-can +There is also a `python-can `__ +mailing list for development discussion. + + +Building & Installing +--------------------- + +The following assumes that the commands are executed from the root of the repository: + +- The project can be built and installed with ``python setup.py build`` and + ``python setup.py install``. +- The unit tests can be run with ``python setup.py test``. The tests can be run with ``python2``, + ``python3``, ``pypy`` or ``pypy3`` to test with other python versions, if they are installed. +- The docs can be built with ``sphinx-build doc/ doc/_build``. + Creating a Release ------------------ @@ -23,7 +38,7 @@ Creating a Release - 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 and github. +- Check the release on PyPi, readthedocs and github. Code Structure @@ -43,9 +58,8 @@ The modules in ``python-can`` are: +---------------------------------+------------------------------------------------------+ |:doc:`message ` | Contains the interface independent Message object. | +---------------------------------+------------------------------------------------------+ -|:doc:`notifier ` | An object which can be used to notify listeners. | +|:doc:`io ` | Contains a range of file readers and writers. | +---------------------------------+------------------------------------------------------+ |:doc:`broadcastmanager ` | Contains interface independent broadcast manager | | | code. | +---------------------------------+------------------------------------------------------+ - diff --git a/doc/interfaces.rst b/doc/interfaces.rst index b150f6cfb..00d1da37d 100644 --- a/doc/interfaces.rst +++ b/doc/interfaces.rst @@ -24,6 +24,19 @@ The available interfaces are: interfaces/vector 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. + +The format of the entry point is ``interface_name=module:classname`` where +``classname`` is a :class:`can.BusABC` concrete implementation. + +:: + + entry_points={ + 'python_can.interface': [ + "interface_name=module:classname", + ] + }, The *Interface Names* are listed in :doc:`configuration`. diff --git a/doc/interfaces/neovi.rst b/doc/interfaces/neovi.rst index 48f4ef1d9..dbb753479 100644 --- a/doc/interfaces/neovi.rst +++ b/doc/interfaces/neovi.rst @@ -1,37 +1,29 @@ -neoVI Interface -=============== +NEOVI Interface +================== .. warning:: - This ``neoVI`` documentation is a work in progress. Feedback and revisions + This ``ICS NeoVI`` documentation is a work in progress. Feedback and revisions are most welcome! Interface to `Intrepid Control Systems `__ neoVI -API range of devices via `pyneovi `__ +API range of devices via `python-ics `__ wrapper on Windows. -.. note:: - - This interface is not supported on Linux, however on Linux neoVI devices - are supported via :doc:`socketcan` with ICS `Kernel-mode SocketCAN module - for Intrepid devices - `__ and - `icsscand `__ - Installation ------------ -This neoVI interface requires the installation of the ICS neoVI DLL and pyneovi +This neovi interface requires the installation of the ICS neoVI DLL and python-ics package. - Download and install the Intrepid Product Drivers `Intrepid Product Drivers `__ -- Install pyneovi using pip and the pyneovi bitbucket repo: +- Install python-ics .. code-block:: bash - pip install https://bitbucket.org/Kemp_J/pyneovi/get/default.zip + pip install python-ics Configuration @@ -49,6 +41,6 @@ An example `can.ini` file for windows 7: Bus --- -.. autoclass:: can.interfaces.neovi_api.NeoVIBus +.. autoclass:: can.interfaces.ics_neovi.NeoViBus diff --git a/doc/interfaces/slcan.rst b/doc/interfaces/slcan.rst index af3a8b565..d47706d51 100755 --- a/doc/interfaces/slcan.rst +++ b/doc/interfaces/slcan.rst @@ -20,4 +20,4 @@ Bus Internals --------- -.. TODO:: Implement and document slcan interface. +.. TODO:: Document internals of slcan interface. diff --git a/doc/interfaces/socketcan_native.rst b/doc/interfaces/socketcan_native.rst index d3ec9c95b..cb6f9aea4 100644 --- a/doc/interfaces/socketcan_native.rst +++ b/doc/interfaces/socketcan_native.rst @@ -3,18 +3,18 @@ SocketCAN (python) Python 3.3 added support for socketcan for linux systems. -The socketcan_native interface directly uses Python's socket module to +The ``socketcan_native`` interface directly uses Python's socket module to access SocketCAN on linux. This is the most direct route to the kernel -and should provide the most responsive. +and should provide the most responsive one. -The implementation features efficient filtering of can_id's, this filtering +The implementation features efficient filtering of can_id's. That filtering occurs in the kernel and is much much more efficient than filtering messages in Python. Python 3.4 added support for the Broadcast Connection Manager (BCM) -protocol, which if enabled should be used for queueing periodic tasks. +protocol, which - if enabled - should be used for queueing periodic tasks. -Documentation for the socket can backend file can be found: +Documentation for the socketcan back end file can be found: https://www.kernel.org/doc/Documentation/networking/can.txt @@ -28,19 +28,19 @@ Bus Internals --------- -createSocket -~~~~~~~~~~~~ +create_socket +~~~~~~~~~~~~~ -.. autofunction:: can.interfaces.socketcan.socketcan_native.createSocket +.. autofunction:: can.interfaces.socketcan.socketcan_native.create_socket -bindSocket -~~~~~~~~~~ +bind_socket +~~~~~~~~~~~ -.. autofunction:: can.interfaces.socketcan.socketcan_native.bindSocket +.. autofunction:: can.interfaces.socketcan.socketcan_native.bind_socket -captureMessage -~~~~~~~~~~~~~~ +capture_message +~~~~~~~~~~~~~~~ -.. autofunction:: can.interfaces.socketcan.socketcan_native.captureMessage +.. autofunction:: can.interfaces.socketcan.socketcan_native.capture_message diff --git a/doc/listeners.rst b/doc/listeners.rst index 7315eb7c3..af3567643 100644 --- a/doc/listeners.rst +++ b/doc/listeners.rst @@ -57,10 +57,29 @@ 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- @@ -78,6 +97,7 @@ as further references can-utils can be used: .. autoclass:: can.ASCReader :members: + Log (.log can-utils Logging format) ----------------------------------- diff --git a/doc/message.rst b/doc/message.rst index 7e2bb881e..cd350d9f1 100644 --- a/doc/message.rst +++ b/doc/message.rst @@ -66,6 +66,9 @@ Message The :abbr:`DLC (Data Link Count)` 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 + number of bytes. + >>> m = Message(data=[1, 2, 3]) >>> m.dlc 3 @@ -116,6 +119,28 @@ Message Timestamp: 0.000000 ID: 00000000 X R DLC: 0 + .. attribute:: is_fd + + :type: bool + + Indicates that this message is a CAN FD message. + + + .. attribute:: bitrate_switch + + :type: bool + + If this is a CAN FD message, this indicates that a higher bitrate + was used for the data transmission. + + + .. attribute:: error_state_indicator + + :type: bool + + If this is a CAN FD message, this indicates an error active state. + + .. attribute:: timestamp :type: float diff --git a/examples/send_one.py b/examples/send_one.py index fc5d7949b..46ae20980 100755 --- a/examples/send_one.py +++ b/examples/send_one.py @@ -3,7 +3,10 @@ def send_one(): - bus = can.interface.Bus() + bus = can.interface.Bus(bustype='pcan', channel='PCAN_USBBUS1', bitrate=250000) + #bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000) + #bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000) + msg = can.Message(arbitration_id=0xc0ffee, data=[0, 25, 0, 1, 3, 1, 4, 1], extended_id=True) diff --git a/setup.py b/setup.py index da2541c50..98bf4a871 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,9 @@ +# -*- coding: utf-8 -*- + """ python-can requires the setuptools package to be installed. """ + import re import logging from setuptools import setup, find_packages @@ -30,6 +33,7 @@ test_suite="nose.collector", tests_require=['mock', 'nose', 'pyserial'], extras_require={ - 'serial': ['pyserial'] + 'serial': ['pyserial'], + 'neovi': ['python-ics'], } ) diff --git a/test/back2back_test.py b/test/back2back_test.py index 146815516..202a5365e 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -1,16 +1,19 @@ +import os import unittest import time import can +IS_TRAVIS = os.environ.get('TRAVIS', 'default') == 'true' BITRATE = 500000 TIMEOUT = 0.1 +TEST_CAN_FD = True INTERFACE_1 = 'virtual' -CHANNEL_1 = 0 +CHANNEL_1 = 'vcan0' INTERFACE_2 = 'virtual' -CHANNEL_2 = 0 +CHANNEL_2 = 'vcan0' class Back2BackTestCase(unittest.TestCase): @@ -22,10 +25,14 @@ class Back2BackTestCase(unittest.TestCase): def setUp(self): self.bus1 = can.interface.Bus(channel=CHANNEL_1, bustype=INTERFACE_1, - bitrate=BITRATE) + bitrate=BITRATE, + fd=TEST_CAN_FD, + single_handle=True) self.bus2 = can.interface.Bus(channel=CHANNEL_2, bustype=INTERFACE_2, - bitrate=BITRATE) + bitrate=BITRATE, + fd=TEST_CAN_FD, + single_handle=True) def tearDown(self): self.bus1.shutdown() @@ -38,6 +45,8 @@ def _check_received_message(self, recv_msg, sent_msg): self.assertEqual(recv_msg.id_type, sent_msg.id_type) 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) + self.assertEqual(recv_msg.bitrate_switch, sent_msg.bitrate_switch) self.assertEqual(recv_msg.dlc, sent_msg.dlc) if not sent_msg.is_remote_frame: self.assertSequenceEqual(recv_msg.data, sent_msg.data) @@ -60,14 +69,17 @@ def _send_and_receive(self, msg): def test_no_message(self): self.assertIsNone(self.bus1.recv(0.1)) + @unittest.skipIf(IS_TRAVIS, "skip on Travis CI") def test_timestamp(self): self.bus2.send(can.Message()) recv_msg1 = self.bus1.recv(TIMEOUT) - time.sleep(1) + time.sleep(5) self.bus2.send(can.Message()) recv_msg2 = self.bus1.recv(TIMEOUT) delta_time = recv_msg2.timestamp - recv_msg1.timestamp - self.assertTrue(0.95 < delta_time < 1.05) + self.assertTrue(4.8 < delta_time < 5.2, + 'Time difference should have been 5s +/- 200ms.' + 'But measured {}'.format(delta_time)) def test_standard_message(self): msg = can.Message(extended_id=False, @@ -94,6 +106,23 @@ def test_dlc_less_than_eight(self): data=[4, 5, 6]) self._send_and_receive(msg) + @unittest.skipUnless(TEST_CAN_FD, "Don't test CAN-FD") + def test_fd_message(self): + msg = can.Message(is_fd=True, + extended_id=True, + arbitration_id=0x56789, + data=[0xff] * 64) + self._send_and_receive(msg) + + @unittest.skipUnless(TEST_CAN_FD, "Don't test CAN-FD") + def test_fd_message_with_brs(self): + msg = can.Message(is_fd=True, + bitrate_switch=True, + extended_id=True, + arbitration_id=0x98765, + data=[0xff] * 48) + self._send_and_receive(msg) + if __name__ == '__main__': unittest.main() diff --git a/test/data/__init__.py b/test/data/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/data/example_data.py b/test/data/example_data.py new file mode 100644 index 000000000..bc097f755 --- /dev/null +++ b/test/data/example_data.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- + +""" +This module contains some example data, like messages of different +types and example comments with different challenges. +""" + +import random + +from can import Message + + +# make tests more reproducible +random.seed(13339115) + +# some random number +TEST_TIME = 1483389946.197 + +# List of messages of different types that can be used in tests +TEST_MESSAGES_BASE = [ + Message( + # empty + ), + Message( + # only data + data=[0x00, 0x42] + ), + Message( + # no data + arbitration_id=0xAB, extended_id=False + ), + Message( + # no data + arbitration_id=0x42, extended_id=True + ), + Message( + # no data + arbitration_id=0xABCDEF, + ), + Message( + # empty data + data=[] + ), + Message( + # empty data + data=[0xFF, 0xFE, 0xFD], + ), + Message( + arbitration_id=0xABCDEF, extended_id=True, + timestamp=TEST_TIME, + data=[1, 2, 3, 4, 5, 6, 7, 8] + ), + Message( + arbitration_id=0x123, extended_id=False, + timestamp=TEST_TIME + 42.42, + data=[0xff, 0xff] + ), + Message( + arbitration_id=0xDADADA, extended_id=True, + timestamp=TEST_TIME + .165, + data=[1, 2, 3, 4, 5, 6, 7, 8] + ), + Message( + arbitration_id=0x123, extended_id=False, + timestamp=TEST_TIME + .365, + data=[254, 255] + ), + Message( + arbitration_id=0x768, extended_id=False, + timestamp=TEST_TIME + 3.165 + ), +] + +TEST_MESSAGES_REMOTE_FRAMES = [ + Message( + arbitration_id=0xDADADA, extended_id=True, is_remote_frame=False, + timestamp=TEST_TIME + .165, + data=[1, 2, 3, 4, 5, 6, 7, 8] + ), + Message( + arbitration_id=0x123, extended_id=False, is_remote_frame=False, + timestamp=TEST_TIME + .365, + data=[254, 255] + ), + Message( + arbitration_id=0x768, extended_id=False, is_remote_frame=True, + timestamp=TEST_TIME + 3.165 + ), + Message( + arbitration_id=0xABCDEF, extended_id=True, is_remote_frame=True, + timestamp=TEST_TIME + 7858.67 + ), +] + +TEST_MESSAGES_ERROR_FRAMES = [ + Message( + is_error_frame=True + ), + Message( + is_error_frame=True, + timestamp=TEST_TIME + 0.170 + ), + Message( + is_error_frame=True, + timestamp=TEST_TIME + 17.157 + ) +] + +TEST_COMMENTS = [ + "This is the first comment", + "", # empty comment + "This third comment contains some strange characters: 'ä\"§$%&/()=?__::_Öüßêè and ends here.", + ( + "This fourth comment is quite long! " \ + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. " \ + "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. " \ + "Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi." \ + ), +] + + +def generate_message(arbitration_id): + """ + Generates a new message with the given ID, some random data + and a non-extended ID. + """ + data = [random.randrange(0, 2 ** 8 - 1) for _ in range(8)] + msg = Message(arbitration_id=arbitration_id, data=data, extended_id=False) + return msg diff --git a/test/listener_test.py b/test/listener_test.py index 2beb66f8c..be795eaf4 100755 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -8,44 +8,44 @@ import can +from data.example_data import generate_message + channel = 'vcan0' can.rc['interface'] = 'virtual' -logging.getLogger("").setLevel(logging.DEBUG) +logging.getLogger('').setLevel(logging.DEBUG) +# make tests more reproducible +random.seed(13339115) -# List of messages of different types that can be used in tests -TEST_MESSAGES = [ - can.Message( - arbitration_id=0xDADADA, extended_id=True, is_remote_frame=False, - timestamp=1483389464.165, - data=[1, 2, 3, 4, 5, 6, 7, 8]), - can.Message( - arbitration_id=0x123, extended_id=False, is_remote_frame=False, - timestamp=1483389464.365, - data=[254, 255]), - can.Message( - arbitration_id=0x768, extended_id=False, is_remote_frame=True, - timestamp=1483389466.165), - can.Message(is_error_frame=True, timestamp=1483389466.170), -] +class ListenerImportTest(unittest.TestCase): -def generate_message(arbitration_id): - data = [random.randrange(0, 2 ** 8 - 1) for _ in range(8)] - m = can.Message(arbitration_id=arbitration_id, data=data, extended_id=False) - return m + def testClassesImportable(self): + self.assertTrue(hasattr(can, 'Listener')) + self.assertTrue(hasattr(can, 'BufferedReader')) + self.assertTrue(hasattr(can, 'Notifier')) + self.assertTrue(hasattr(can, 'Logger')) + self.assertTrue(hasattr(can, 'ASCWriter')) + self.assertTrue(hasattr(can, 'ASCReader')) -class ListenerImportTest(unittest.TestCase): + self.assertTrue(hasattr(can, 'BLFReader')) + self.assertTrue(hasattr(can, 'BLFWriter')) - def testClassesImportable(self): - assert hasattr(can, 'Listener') - assert hasattr(can, 'BufferedReader') - assert hasattr(can, 'Notifier') - assert hasattr(can, 'ASCWriter') - assert hasattr(can, 'CanutilsLogWriter') - assert hasattr(can, 'SqlReader') + self.assertTrue(hasattr(can, 'CSVWriter')) + + self.assertTrue(hasattr(can, 'CanutilsLogWriter')) + self.assertTrue(hasattr(can, 'CanutilsLogReader')) + + self.assertTrue(hasattr(can, 'SqliteReader')) + self.assertTrue(hasattr(can, 'SqliteWriter')) + + self.assertTrue(hasattr(can, 'Printer')) + + self.assertTrue(hasattr(can, 'LogReader')) + + self.assertTrue(hasattr(can.io.player, 'MessageSync')) class BusTest(unittest.TestCase): @@ -75,7 +75,7 @@ def test_filetype_to_instance(extension, klass): test_filetype_to_instance('log', can.CanutilsLogWriter) 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("db", can.SqliteWriter) test_filetype_to_instance("txt", can.Printer) def testBufferedListenerReceives(self): @@ -84,139 +84,6 @@ def testBufferedListenerReceives(self): m = a_listener.get_message(0.2) self.assertIsNotNone(m) - def testSQLWriterReceives(self): - f = tempfile.NamedTemporaryFile('w', delete=False) - f.close() - a_listener = can.SqliteWriter(f.name) - a_listener(generate_message(0xDADADA)) - # Small delay so we don't stop before we actually block trying to read - sleep(0.5) - a_listener.stop() - - con = sqlite3.connect(f.name) - c = con.cursor() - c.execute("select * from messages") - msg = c.fetchone() - con.close() - self.assertEqual(msg[1], 0xDADADA) - - 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() - - second_listener = can.SqliteWriter(f.name) - second_listener(generate_message(0x02)) - - sleep(second_listener.MAX_TIME_BETWEEN_WRITES) - - second_listener.stop() - - con = sqlite3.connect(f.name) - - with con: - c = con.cursor() - - c.execute("select COUNT() from messages") - self.assertEqual(2, c.fetchone()[0]) - - c.execute("select * from messages") - msg1 = c.fetchone() - msg2 = c.fetchone() - - assert msg1[1] == 0x01 - assert msg2[1] == 0x02 - - - def testAscListener(self): - a_listener = can.ASCWriter("test.asc", channel=2) - a_listener.log_event("This is some comment") - msg = can.Message(extended_id=True, - timestamp=a_listener.started + 0.5, - arbitration_id=0xabcdef, - data=[1, 2, 3, 4, 5, 6, 7, 8]) - a_listener(msg) - msg = can.Message(extended_id=False, - timestamp=a_listener.started + 1, - arbitration_id=0x123, - data=[0xff, 0xff]) - a_listener(msg) - msg = can.Message(extended_id=True, - timestamp=a_listener.started + 1.5, - is_remote_frame=True, - dlc=8, - arbitration_id=0xabcdef) - a_listener(msg) - msg = can.Message(is_error_frame=True, - timestamp=a_listener.started + 1.6, - arbitration_id=0xabcdef) - a_listener(msg) - a_listener.stop() - with open("test.asc", "r") as f: - output_contents = f.read() - - self.assertTrue('This is some comment' in output_contents) - print("Output from ASCWriter:") - print(output_contents) - - -class FileReaderTest(BusTest): - - def test_sql_reader(self): - f = tempfile.NamedTemporaryFile('w', delete=False) - f.close() - a_listener = can.SqliteWriter(f.name) - a_listener(generate_message(0xDADADA)) - - sleep(a_listener.MAX_TIME_BETWEEN_WRITES) - while not a_listener.buffer.empty(): - sleep(0.1) - a_listener.stop() - - reader = can.SqlReader(f.name) - - ms = [] - for m in reader: - ms.append(m) - - self.assertEqual(len(ms), 1) - self.assertEqual(0xDADADA, ms[0].arbitration_id) - - -class BLFTest(unittest.TestCase): - - def test_reader(self): - logfile = os.path.join(os.path.dirname(__file__), "data", "logfile.blf") - messages = list(can.BLFReader(logfile)) - self.assertEqual(len(messages), 1) - self.assertEqual(messages[0], - can.Message( - extended_id=False, - arbitration_id=0x64, - data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8])) - - def test_reader_writer(self): - f = tempfile.NamedTemporaryFile('w', delete=False) - f.close() - filename = f.name - - writer = can.BLFWriter(filename) - for msg in TEST_MESSAGES: - writer(msg) - writer.log_event("One comment which should be attached to last message") - writer.log_event("Another comment", TEST_MESSAGES[-1].timestamp + 2) - writer.stop() - - messages = list(can.BLFReader(filename)) - self.assertEqual(len(messages), len(TEST_MESSAGES)) - for msg1, msg2 in zip(messages, TEST_MESSAGES): - self.assertEqual(msg1, msg2) - self.assertAlmostEqual(msg1.timestamp, msg2.timestamp) if __name__ == '__main__': unittest.main() diff --git a/test/logformats_test.py b/test/logformats_test.py index 043eb97d5..3cc494adb 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -1,60 +1,210 @@ +""" +This test module test the separate reader/writer combinations of the can.io.* +modules by writing some messages to a temporary file and reading it again. +Then it checks if the messages that were read are same ones as the +ones that were written. It also checks that the order of the messages +is correct. The types of messages that are tested differs between the +different writer/reader pairs - e.g., some don't handle error frames and +comments. +""" + import unittest import tempfile +from time import sleep +import sqlite3 +import os + +try: + # Python 3 + from itertools import zip_longest +except ImportError: + # Python 2 + from itertools import izip_longest as zip_longest + import can -# List of messages of different types that can be used in tests -TEST_MESSAGES = [ - can.Message( - arbitration_id=0xDADADA, extended_id=True, is_remote_frame=False, - timestamp=1483389464.165, - data=[1, 2, 3, 4, 5, 6, 7, 8]), - can.Message( - arbitration_id=0x123, extended_id=False, is_remote_frame=False, - timestamp=1483389464.365, - data=[254, 255]), - can.Message( - arbitration_id=0x768, extended_id=False, is_remote_frame=True, - timestamp=1483389466.165), - can.Message(is_error_frame=True, timestamp=1483389466.170), -] +from data.example_data import TEST_MESSAGES_BASE, TEST_MESSAGES_REMOTE_FRAMES, \ + TEST_MESSAGES_ERROR_FRAMES, TEST_COMMENTS, \ + generate_message -class TestCanutilsLog(unittest.TestCase): +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): + """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. - def test_reader_writer(self): - f = tempfile.NamedTemporaryFile('w', delete=False) - f.close() - filename = f.name - writer = can.CanutilsLogWriter(filename) + :param test_case: the test case the use the assert methods on + :param sleep_time: specifies the time to sleep after writing all messages. + gets ignored when set to None + :param check_remote_frames: if true, also tests remote frames + :param check_error_frames: if true, also tests error frames + :param 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. + """ + + 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 - for msg in TEST_MESSAGES: + # create writer + writer = writer_constructor(filename) + + # write + if check_comments: + # write 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): + # 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) - writer.stop() + print("writing message: ", msg) + + # sleep and close the writer + if sleep_time is not None: + sleep(sleep_time) + + writer.stop() + + # read all written messages + read_messages = list(reader_constructor(filename)) + + # 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") + + # check the order and content of the individual messages + for i, (read, original) in enumerate(zip(read_messages, original_messages)): + try: + test_case.assertEqual(read, original) + test_case.assertAlmostEqual(read.timestamp, original.timestamp) + 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_error_frames=False, # TODO this should get fixed, see Issue #217 + check_comments=False) - messages = list(can.CanutilsLogReader(filename)) - self.assertEqual(len(messages), len(TEST_MESSAGES)) - for msg1, msg2 in zip(messages, TEST_MESSAGES): - self.assertEqual(msg1, msg2) - self.assertAlmostEqual(msg1.timestamp, msg2.timestamp) class TestAscFileFormat(unittest.TestCase): + """Tests can.ASCWriter and can.ASCReader""" + + def test_writer_and_reader(self): + _test_writer_and_reader(self, can.ASCWriter, can.ASCReader, + check_error_frames=False, # TODO this should get fixed, see Issue #218 + check_comments=True) + + +class TestSqlFileFormat(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, + check_comments=False) - def test_reader_writer(self): + def testSQLWriterWritesToSameFile(self): f = tempfile.NamedTemporaryFile('w', delete=False) f.close() - filename = f.name - writer = can.ASCWriter(filename) - for msg in TEST_MESSAGES: - writer(msg) - writer.stop() + first_listener = can.SqliteWriter(f.name) + first_listener(generate_message(0x01)) + + sleep(first_listener.MAX_TIME_BETWEEN_WRITES) + first_listener.stop() + + second_listener = can.SqliteWriter(f.name) + second_listener(generate_message(0x02)) + + sleep(second_listener.MAX_TIME_BETWEEN_WRITES) + + second_listener.stop() + + con = sqlite3.connect(f.name) + + with con: + c = con.cursor() + + c.execute("select COUNT() from messages") + self.assertEqual(2, c.fetchone()[0]) + + c.execute("select * from messages") + msg1 = c.fetchone() + msg2 = c.fetchone() + + self.assertEqual(msg1[1], 0x01) + self.assertEqual(msg2[1], 0x02) + + +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, + sleep_time=None, + check_comments=False) + + def test_reader(self): + logfile = os.path.join(os.path.dirname(__file__), "data", "logfile.blf") + messages = list(can.BLFReader(logfile)) + self.assertEqual(len(messages), 1) + self.assertEqual(messages[0], + can.Message( + extended_id=False, + arbitration_id=0x64, + data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8])) - messages = list(can.ASCReader(filename)) - self.assertEqual(len(messages), len(TEST_MESSAGES)) - for msg1, msg2 in zip(messages, TEST_MESSAGES): - self.assertEqual(msg1, msg2) - self.assertAlmostEqual(msg1.timestamp, msg2.timestamp) if __name__ == '__main__': unittest.main() - diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index e3aed2126..980fe1eee 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -1,28 +1,31 @@ +import os from time import sleep import unittest + import can +IS_TRAVIS = os.environ.get('TRAVIS', 'default') == 'true' class SimpleCyclicSendTaskTest(unittest.TestCase): + @unittest.skipIf(IS_TRAVIS, "skip on Travis CI") def test_cycle_time(self): msg = can.Message(extended_id=False, arbitration_id=0x100, data=[0,1,2,3,4,5,6,7]) - bus = can.interface.Bus(bustype='virtual') + bus1 = can.interface.Bus(bustype='virtual') bus2 = can.interface.Bus(bustype='virtual') - task = bus.send_periodic(msg, 0.01, 1) + task = bus1.send_periodic(msg, 0.01, 1) self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) - sleep(1.5) + sleep(5) size = bus2.queue.qsize() - print(size) # About 100 messages should have been transmitted - self.assertTrue(90 < size < 110) + self.assertTrue(90 < size < 110, + '100 +/- 10 messages should have been transmitted. But queue contained {}'.format(size)) last_msg = bus2.recv() self.assertEqual(last_msg, msg) - bus.shutdown() + bus1.shutdown() bus2.shutdown() - if __name__ == '__main__': unittest.main()