diff --git a/.appveyor.yml b/.appveyor.yml index 644e0c6bc..c724c96a2 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -33,4 +33,5 @@ test_script: # Note that you must use the environment variable %PYTHON% to refer to # the interpreter you're using - Appveyor does not do anything special # to put the Python version you want to use on PATH. - - "%PYTHON%\\python.exe setup.py test -v" + - "%PYTHON%\\python.exe setup.py test -v" # --timeout=300 -> TODO: + # need to switch to pytest like on Unix first, but that's for another PR diff --git a/.travis.yml b/.travis.yml index 065f12081..1e6f4be50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,4 +51,4 @@ install: - travis_retry pip install .[test] script: - - py.test -v + - py.test -v --timeout=300 diff --git a/can/bus.py b/can/bus.py index 993afdf24..9e70cb67e 100644 --- a/can/bus.py +++ b/can/bus.py @@ -2,7 +2,7 @@ # coding: utf-8 """ -Contains the ABC bus implementation. +Contains the ABC bus implementation and its documentation. """ from __future__ import print_function, absolute_import @@ -10,71 +10,129 @@ from abc import ABCMeta, abstractmethod import logging import threading +from time import time from collections import namedtuple -from can.broadcastmanager import ThreadBasedCyclicSendTask - -logger = logging.getLogger(__name__) +from .broadcastmanager import ThreadBasedCyclicSendTask BusState = namedtuple('BusState', 'ACTIVE, PASSIVE, ERROR') class BusABC(object): - """CAN Bus Abstract Base Class - - Concrete implementations must implement the following methods: - * send - * recv - - As well as setting the `channel_info` attribute to a string describing the - interface. - - They may implement :meth:`~can.BusABC._detect_available_configs` to allow - the interface to report which configurations are currently available for - new connections. - + """The CAN Bus Abstract Base Class that serves as the basis + for all concrete interfaces. """ - #: a string describing the underlying bus channel + #: a string describing the underlying bus and/or channel channel_info = 'unknown' @abstractmethod def __init__(self, channel=None, can_filters=None, **config): - """ + """Construct and open a CAN bus instance of the specified type. + + Subclasses should call though this method with all given parameters + as it handles generic tasks like applying filters. + :param channel: The can interface identifier. Expected type is backend dependent. :param list can_filters: - A list of dictionaries each containing a "can_id", a "can_mask", - and an "extended" key. - - >>> [{"can_id": 0x11, "can_mask": 0x21, "extended": False}] - - A filter matches, when `` & can_mask == can_id & can_mask`` + See :meth:`~can.BusABC.set_filters` for details. :param dict config: Any backend dependent configurations are passed in this dictionary """ - pass + self.set_filters(can_filters) def __str__(self): return self.channel_info - @abstractmethod def recv(self, timeout=None): """Block waiting for a message from the Bus. - :param float timeout: Seconds to wait for a message. + :param float timeout: + seconds to wait for a message or None to wait indefinitely + :rtype: can.Message or None :return: None on timeout or a :class:`can.Message` object. + :raises can.CanError: + if an error occurred while reading + """ + start = time() + time_left = timeout + + while True: + + # try to get a message + msg, already_filtered = self._recv_internal(timeout=time_left) + + # return it, if it matches + if msg and (already_filtered or self._matches_filters(msg)): + return msg + + # if not, and timeout is None, try indefinitely + elif timeout is None: + continue + + # try next one only if there still is time, and with reduced timeout + else: + + time_left = timeout - (time() - start) + + if time_left > 0: + continue + else: + return None + + def _recv_internal(self, timeout): + """ + Read a message from the bus and tell whether it was filtered. + This methods may be called by :meth:`~can.BusABC.recv` + to read a message multiple times if the filters set by + :meth:`~can.BusABC.set_filters` do not match and the call has + not yet timed out. + + New implementations should always override this method instead of + :meth:`~can.BusABC.recv`, to be able to take advantage of the + software based filtering provided by :meth:`~can.BusABC.recv` + as a fallback. This method should never be called directly. + + .. note:: + + This method is not an `@abstractmethod` (for now) to allow older + external implementations to continue using their existing + :meth:`~can.BusABC.recv` implementation. + + .. note:: + + The second return value (whether filtering was already done) may change + over time for some interfaces, like for example in the Kvaser interface. + Thus it cannot be simplified to a constant value. + + :param float timeout: seconds to wait for a message, + see :meth:`can.BusABC.send` + + :rtype: tuple[can.Message, bool] or tuple[None, bool] + :return: + 1. a message that was read or None on timeout + 2. a bool that is True if message filtering has already + been done and else False + + :raises can.CanError: + if an error occurred while reading + :raises NotImplementedError: + if the bus provides it's own :meth:`~can.BusABC.recv` + implementation (legacy implementation) + """ raise NotImplementedError("Trying to read from a write only bus?") @abstractmethod def send(self, msg, timeout=None): - """Transmit a message to CAN bus. + """Transmit a message to the CAN bus. + Override this method to enable the transmit path. :param can.Message msg: A message object. @@ -84,7 +142,7 @@ def send(self, msg, timeout=None): If timeout is exceeded, an exception will be raised. Might not be supported by all interfaces. - :raise: :class:`can.CanError` + :raises can.CanError: if the message could not be written. """ raise NotImplementedError("Trying to write to a readonly bus?") @@ -103,6 +161,8 @@ def send_periodic(self, msg, period, duration=None): :return: A started task instance :rtype: can.CyclicSendTaskABC + .. note:: + Note the duration before the message stops being sent may not be exactly the same as the duration specified by the user. In general the message will be sent at the given rate until at @@ -110,7 +170,7 @@ def send_periodic(self, msg, period, duration=None): """ if not hasattr(self, "_lock"): - # Create send lock for this bus + # Create a send lock for this bus self._lock = threading.Lock() return ThreadBasedCyclicSendTask(self, self._lock, msg, period, duration) @@ -121,27 +181,91 @@ def __iter__(self): ... print(msg) - :yields: :class:`can.Message` msg objects. + :yields: + :class:`can.Message` msg objects. """ while True: msg = self.recv(timeout=1.0) if msg is not None: yield msg - def set_filters(self, can_filters=None): + @property + def filters(self): + return self._filters + + @filters.setter + def filters(self, filters): + self.set_filters(filters) + + def set_filters(self, filters=None): """Apply filtering to all messages received by this Bus. - Calling without passing any filters will reset the applied filters. + All messages that match at least one filter are returned. + If `filters` is `None`, all messages are matched. + If it is a zero size interable, no messages are matched. - :param list can_filters: - A list of dictionaries each containing a "can_id" and a "can_mask". + Calling without passing any filters will reset the applied + filters to `None`. - >>> [{"can_id": 0x11, "can_mask": 0x21}] + :param Iterator[dict] filters: + A iterable of dictionaries each containing a "can_id", a "can_mask", + and an optional "extended" key. - A filter matches, when `` & can_mask == can_id & can_mask`` + >>> [{"can_id": 0x11, "can_mask": 0x21, "extended": False}] + + A filter matches, when `` & can_mask == can_id & can_mask``. + If ``extended`` is set as well, it only matches messages where + `` == extended``. Else it matches every messages based + only on the arbitration ID and mask. + + """ + self._filters = filters + self._apply_filters(self._filters) + def _apply_filters(self, filters): """ - raise NotImplementedError("Trying to set_filters on unsupported bus") + Hook for applying the filters to the underlying kernel or + hardware if supported/implemented by the interface. + + :param Iterator[dict] filters: + See :meth:`~can.BusABC.set_filters` for details. + """ + pass + + def _matches_filters(self, msg): + """Checks whether the given message matches at least one of the + current filters. See :meth:`~can.BusABC.set_filters` for details + on how the filters work. + + This method should not be overridden. + + :param can.Message msg: + the message to check if matching + :rtype: bool + :return: whether the given message matches at least one filter + """ + + # if no filters are set, all messages are matched + if self._filters is None: + return True + + for filter in self._filters: + # check if this filter even applies to the message + if 'extended' in filter and \ + filter['extended'] != msg.is_extended_id: + continue + + # then check for the mask and id + can_id = filter['can_id'] + can_mask = filter['can_mask'] + + # basically, we compute `msg.arbitration_id & can_mask == can_id & can_mask` + # by using the shorter, but equivalent from below: + if (can_id ^ msg.arbitration_id) & can_mask == 0: + return True + + # nothing matched + return False def flush_tx_buffer(self): """Discard every message that may be queued in the output buffer(s). @@ -153,7 +277,7 @@ def shutdown(self): Called to carry out any interface specific cleanup required in shutting down a bus. """ - self.flush_tx_buffer() + pass @property def state(self): diff --git a/can/interface.py b/can/interface.py index f6ba2bc56..2413d72f1 100644 --- a/can/interface.py +++ b/can/interface.py @@ -22,7 +22,6 @@ if sys.version_info.major > 2: basestring = str - log = logging.getLogger('can.interface') log_autodetect = log.getChild('detect_available_configs') diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 7cfdd6595..18ec93e96 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -14,8 +14,7 @@ import logging from collections import deque -from can import Message, CanError -from can.bus import BusABC +from can import Message, CanError, BusABC logger = logging.getLogger(__name__) @@ -30,6 +29,10 @@ class ICSApiError(CanError): + """ + Indicates an error with the ICS API. + """ + # A critical error which affects operation or accuracy. ICS_SPY_ERR_CRITICAL = 0x10 # An error which is not understood. @@ -39,10 +42,8 @@ class ICSApiError(CanError): # 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 - ): + 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 @@ -83,13 +84,10 @@ class NeoViBus(BusABC): 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". - - >>> [{"can_id": 0x11, "can_mask": 0x21}] + See :meth:`can.BusABC.set_filters` for details. :param use_system_timestamp: Use system timestamp for can messages instead of the hardware time @@ -101,18 +99,17 @@ def __init__(self, channel=None, can_filters=None, **config): Channel bitrate in bit/s. (optional, will enable the auto bitrate feature if not supplied) """ - super(NeoViBus, self).__init__(channel, can_filters, **config) if ics is None: raise ImportError('Please install python-ics') + super(NeoViBus, self).__init__(channel=channel, can_filters=can_filters, **config) + logger.info("CAN Filters: {}".format(can_filters)) logger.info("Got configuration of: {}".format(config)) - self._use_system_timestamp = bool( - config.get('use_system_timestamp', False) - ) + self._use_system_timestamp = bool(config.get('use_system_timestamp', False)) - # TODO: Add support for multiples channels + # TODO: Add support for multiple channels try: channel = int(channel) except ValueError: @@ -133,13 +130,14 @@ def __init__(self, channel=None, can_filters=None, **config): } if bitrate is not None: - if int(bitrate) not in VALID_BITRATES: + bitrate = int(bitrate) + if bitrate not in VALID_BITRATES: raise ValueError( 'Invalid bitrate. Valid bitrates are {}'.format( VALID_BITRATES ) ) - baud_rate_setting = BAUDRATE_SETTING[int(bitrate)] + baud_rate_setting = BAUDRATE_SETTING[bitrate] settings = { 'SetBaudrate': ics.AUTO, 'Baudrate': baud_rate_setting, @@ -154,12 +152,10 @@ def __init__(self, channel=None, can_filters=None, **config): ) logger.info("Using device: {}".format(self.channel_info)) - 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 + self.network = channel if channel is not None else None # TODO: Change the scaling based on the device type self.ts_scaling = ( @@ -227,7 +223,7 @@ def _set_can_settings(self, channel, setting): setattr(channel_settings, setting, value) ics.set_device_settings(self.dev, device_settings) - def _process_msg_queue(self, timeout=None): + def _process_msg_queue(self, timeout): try: messages, errors = ics.get_messages(self.dev, False, timeout) except ics.RuntimeError: @@ -235,8 +231,6 @@ def _process_msg_queue(self, timeout=None): 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) @@ -247,26 +241,6 @@ def _process_msg_queue(self, timeout=None): 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. @@ -304,22 +278,20 @@ def _ics_msg_to_message(self, ics_msg): channel=ics_msg.NetworkID ) - def recv(self, timeout=None): - msg = None + def _recv_internal(self, timeout): if not self.rx_buffer: - self._process_msg_queue(timeout=timeout) - + self._process_msg_queue(timeout) try: ics_msg = self.rx_buffer.popleft() msg = self._ics_msg_to_message(ics_msg) except IndexError: - pass - return msg + return None, False + else: + return msg, False def send(self, msg, timeout=None): if not self.opened: - return - data = tuple(msg.data) + raise CanError("bus not yet opened") flags = 0 if msg.is_extended_id: @@ -329,8 +301,8 @@ def send(self, msg, timeout=None): message = ics.SpyMessage() message.ArbIDOrHeader = msg.arbitration_id - message.NumberBytesData = len(data) - message.Data = data + message.NumberBytesData = len(msg.data) + message.Data = tuple(msg.data) message.StatusBitField = flags message.StatusBitField2 = 0 message.NetworkID = self.network @@ -339,28 +311,3 @@ def send(self, msg, timeout=None): 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/iscan.py b/can/interfaces/iscan.py index 8bfb89b87..7f127b241 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -5,6 +5,8 @@ Interface for isCAN from Thorsis Technologies GmbH, former ifak system GmbH. """ +from __future__ import absolute_import, division + import ctypes import time import logging @@ -76,16 +78,21 @@ def __init__(self, channel, bitrate=500000, poll_interval=0.01, **kwargs): """ if iscan is None: raise ImportError("Could not load isCAN driver") + self.channel = ctypes.c_ubyte(int(channel)) self.channel_info = "IS-CAN: %s" % channel + if bitrate not in self.BAUDRATES: valid_bitrates = ", ".join(str(bitrate) for bitrate in self.BAUDRATES) raise ValueError("Invalid bitrate, choose one of " + valid_bitrates) + self.poll_interval = poll_interval iscan.isCAN_DeviceInitEx(self.channel, self.BAUDRATES[bitrate]) - super(IscanBus, self).__init__(channel, **kwargs) - def recv(self, timeout=None): + super(IscanBus, self).__init__(channel=channel, bitrate=bitrate, + poll_interval=poll_interval, **kwargs) + + def _recv_internal(self, timeout): raw_msg = MessageExStruct() end_time = time.time() + timeout if timeout is not None else None while True: @@ -97,19 +104,21 @@ def recv(self, timeout=None): raise if end_time is not None and time.time() > end_time: # No message within timeout - return None + return None, False # Sleep a short time to avoid hammering time.sleep(self.poll_interval) else: # A message was received break - return Message(arbitration_id=raw_msg.message_id, - extended_id=bool(raw_msg.is_extended), - timestamp=time.time(), # Better than nothing... - is_remote_frame=bool(raw_msg.remote_req), - dlc=raw_msg.data_len, - data=raw_msg.data[:raw_msg.data_len], - channel=self.channel.value) + + msg = Message(arbitration_id=raw_msg.message_id, + extended_id=bool(raw_msg.is_extended), + timestamp=time.time(), # Better than nothing... + is_remote_frame=bool(raw_msg.remote_req), + dlc=raw_msg.data_len, + data=raw_msg.data[:raw_msg.data_len], + channel=self.channel.value) + return msg, False def send(self, msg, timeout=None): raw_msg = MessageExStruct(msg.arbitration_id, @@ -124,6 +133,7 @@ def shutdown(self): class IscanError(CanError): + # TODO: document ERROR_CODES = { 1: "No access to device", diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index ed4471766..a34a23407 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -5,23 +5,28 @@ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems Copyright (C) 2016 Giuseppe Corbelli + +TODO: We could implement this interface such that setting other filters + could work when the initial filters were set to zero using the + software fallback. Or could the software filters even be changed + after the connection was opened? We need to document that bahaviour! + See also the NICAN interface. + """ +from __future__ import absolute_import, division + import ctypes import functools import logging import sys -import time -from can import CanError, BusABC -from can import Message +from can import CanError, BusABC, Message from can.broadcastmanager import (LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC) from can.ctypesutil import CLibrary, HANDLE, PHANDLE, HRESULT as ctypes_HRESULT -from can.interfaces.ixxat import constants, structures - -from .constants import VCI_MAX_ERRSTRLEN +from . import constants, structures from .exceptions import * __all__ = ["VCITimeout", "VCIError", "VCIDeviceNotFoundError", "IXXATBus", "vciFormatError"] @@ -30,9 +35,9 @@ try: # since Python 3.3 - _timer_function = time.perf_counter -except AttributeError: - _timer_function = time.clock + from time import perf_counter as _timer_function +except ImportError: + from time import clock as _timer_function # Hack to have vciFormatError as a free function, see below vciFormatError = None @@ -86,9 +91,9 @@ def __vciFormatError(library_instance, function, HRESULT): :return: Formatted string """ - buf = ctypes.create_string_buffer(VCI_MAX_ERRSTRLEN) - ctypes.memset(buf, 0, VCI_MAX_ERRSTRLEN) - library_instance.vciFormatError(HRESULT, buf, VCI_MAX_ERRSTRLEN) + buf = ctypes.create_string_buffer(constants.VCI_MAX_ERRSTRLEN) + ctypes.memset(buf, 0, constants.VCI_MAX_ERRSTRLEN) + library_instance.vciFormatError(HRESULT, buf, constants.VCI_MAX_ERRSTRLEN) return "function {} failed ({})".format(function._name, buf.value.decode('utf-8', 'replace')) @@ -231,6 +236,14 @@ def __check_status(result, function, arguments): class IXXATBus(BusABC): """The CAN Bus implemented for the IXXAT interface. + + .. warning:: + + This interface does implement efficient filtering of messages, but + the filters have to be set in :meth:`~can.interfaces.ixxat.IXXATBus.__init__` + using the ``can_filters`` parameter. Using :meth:`~can.interfaces.ixxat.IXXATBus.set_filters` + does not work. + """ CHANNEL_BITRATES = { @@ -264,9 +277,7 @@ def __init__(self, channel, can_filters=None, **config): 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". - - >>> [{"can_id": 0x11, "can_mask": 0x21}] + See :meth:`can.BusABC.set_filters`. :param int UniqueHardwareId: UniqueHardwareId to connect (optional, will use the first found if not supplied) @@ -344,7 +355,7 @@ def __init__(self, channel, can_filters=None, **config): self._tick_resolution = float(self._channel_capabilities.dwClockFreq / self._channel_capabilities.dwTscDivisor) # Setup filters before starting the channel - if can_filters is not None and len(can_filters): + if can_filters: log.info("The IXXAT VCI backend is filtering messages") # Disable every message coming in for extended in (0, 1): @@ -379,7 +390,7 @@ def __init__(self, channel, can_filters=None, **config): except (VCITimeout, VCIRxQueueEmptyError): break - super(IXXATBus, self).__init__() + super(IXXATBus, self).__init__(channel=channel, can_filters=None, **config) def _inWaiting(self): try: @@ -394,7 +405,7 @@ def flush_tx_buffer(self): # TODO #64: no timeout? _canlib.canChannelWaitTxEvent(self._channel_handle, constants.INFINITE) - def recv(self, timeout=None): + def _recv_internal(self, timeout): """ Read a message from IXXAT device. """ # TODO: handling CAN error messages? @@ -405,7 +416,7 @@ def recv(self, timeout=None): try: _canlib.canChannelPeekMessage(self._channel_handle, ctypes.byref(self._message)) except (VCITimeout, VCIRxQueueEmptyError): - return None + return None, True else: if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA: data_received = True @@ -449,7 +460,7 @@ def recv(self, timeout=None): if not data_received: # Timed out / can message type is not DATA - return None + return None, True # The _message.dwTime is a 32bit tick value and will overrun, # so expect to see the value restarting from 0 @@ -464,7 +475,7 @@ def recv(self, timeout=None): ) log.debug('Recv()ed message %s', rx_msg) - return rx_msg + return rx_msg, True def send(self, msg, timeout=None): log.debug("Sending message: %s", msg) @@ -507,6 +518,16 @@ def shutdown(self): _canlib.canControlClose(self._control_handle) _canlib.vciDeviceClose(self._device_handle) + __set_filters_has_been_called = False + def set_filters(self, can_filers=None): + """Unsupported. See note on :class:`~can.interfaces.ixxat.IXXATBus`. + """ + if self.__set_filters_has_been_called: + log.warn("using filters is not supported like this, see note on IXXATBus") + else: + # allow the constructor to call this without causing a warning + self.__set_filters_has_been_called = True + class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC): diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 51cd84d55..070038997 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -9,6 +9,8 @@ Copyright (C) 2010 Dynamic Controls """ +from __future__ import absolute_import + import sys import time import logging @@ -16,7 +18,7 @@ from can import CanError, BusABC from can import Message -from can.interfaces.kvaser import constants as canstat +from . import constants as canstat log = logging.getLogger('can.kvaser') @@ -294,10 +296,7 @@ def __init__(self, channel, can_filters=None, **config): 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". - - >>> [{"can_id": 0x11, "can_mask": 0x21}] - + See :meth:`can.BusABC.set_filters`. Backend Configuration @@ -314,8 +313,8 @@ def __init__(self, channel, can_filters=None, **config): Time segment 2, that is, the number of quanta from the sampling point to the end of the bit. :param int sjw: - The Synchronisation Jump Width. Decides the maximum number of time quanta - that the controller can resynchronise every bit. + The Synchronization Jump Width. Decides the maximum number of time quanta + that the controller can resynchronize every bit. :param int no_samp: Either 1 or 3. Some CAN controllers can also sample each bit three times. In this case, the bit will be sampled three quanta in a row, @@ -338,7 +337,9 @@ def __init__(self, channel, can_filters=None, **config): :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)) bitrate = config.get('bitrate', 500000) @@ -419,8 +420,6 @@ def __init__(self, channel, can_filters=None, **config): self._write_handle = canOpenChannel(channel, flags) canBusOn(self._read_handle) - self.set_filters(can_filters) - can_driver_mode = canstat.canDRIVER_SILENT if driver_mode == DRIVER_MODE_SILENT else canstat.canDRIVER_NORMAL canSetBusOutputControl(self._write_handle, can_driver_mode) log.debug('Going bus on TX handle') @@ -434,43 +433,33 @@ def __init__(self, channel, can_filters=None, **config): log.info(str(exc)) self._timestamp_offset = time.time() - (timer.value * TIMESTAMP_FACTOR) - super(KvaserBus, self).__init__() - - def set_filters(self, can_filters=None): - """Apply filtering to all messages received by this Bus. + self._is_filtered = False + super(KvaserBus, self).__init__(channel=channel, can_filters=can_filters, **config) - Calling without passing any filters will reset the applied filters. - - Since Kvaser only supports setting one filter per handle, the filtering - will be disabled if more than one filter is requested. - - :param list can_filters: - A list of dictionaries each containing a "can_id", "can_mask" and - "extended". - - >>> [{"can_id": 0x11, "can_mask": 0x21, "extended": False}] - - A filter matches, when `` & can_mask == can_id & can_mask`` - """ - if can_filters and len(can_filters) == 1: - can_id = can_filters[0]['can_id'] - can_mask = can_filters[0]['can_mask'] - extended = 1 if can_filters[0].get('extended') else 0 + def _apply_filters(self, filters): + if filters and len(filters) == 1: + can_id = filters[0]['can_id'] + can_mask = filters[0]['can_mask'] + extended = 1 if filters[0].get('extended') else 0 try: for handle in (self._read_handle, self._write_handle): canSetAcceptanceFilter(handle, can_id, can_mask, extended) except (NotImplementedError, CANLIBError) as e: + self._is_filtered = False log.error('Filtering is not supported - %s', e) else: + self._is_filtered = True log.info('canlib is filtering on ID 0x%X, mask 0x%X', can_id, can_mask) else: + self._is_filtered = False log.info('Hardware filtering has been disabled') try: for handle in (self._read_handle, self._write_handle): for extended in (0, 1): canSetAcceptanceFilter(handle, 0, 0, extended) except (NotImplementedError, CANLIBError): + # TODO add logging? pass def flush_tx_buffer(self): @@ -478,9 +467,9 @@ def flush_tx_buffer(self): """ canIoCtl(self._write_handle, canstat.canIOCTL_FLUSH_TX_BUFFER, 0, 0) - def recv(self, timeout=None): + def _recv_internal(self, timeout=None): """ - Read a message from kvaser device. + Read a message from kvaser device and return whether filtering has taken place. """ arb_id = ctypes.c_long(0) data = ctypes.create_string_buffer(64) @@ -531,10 +520,10 @@ def recv(self, timeout=None): rx_msg.flags = flags rx_msg.raw_timestamp = msg_timestamp #log.debug('Got message: %s' % rx_msg) - return rx_msg + return rx_msg, self._is_filtered else: #log.debug('read complete -> status not okay') - return None + return None, self._is_filtered def send(self, msg, timeout=None): #log.debug("Writing a message: {}".format(msg)) diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index 7d3a59fb3..a2364767d 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -7,6 +7,13 @@ Implementation references: * http://www.ni.com/pdf/manuals/370289c.pdf * https://github.com/buendiya/NicanPython + +TODO: We could implement this interface such that setting other filters + could work when the initial filters were set to zero using the + software fallback. Or could the software filters even be changed + after the connection was opened? We need to document that bahaviour! + See also the IXXAT interface. + """ import ctypes @@ -113,6 +120,14 @@ def get_error_message(status_code): class NicanBus(BusABC): """ The CAN Bus implemented for the NI-CAN interface. + + .. warning:: + + This interface does implement efficient filtering of messages, but + the filters have to be set in :meth:`~can.interfaces.nican.NicanBus.__init__` + using the ``can_filters`` parameter. Using :meth:`~can.interfaces.nican.NicanBus.set_filters` + does not work. + """ def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, @@ -125,9 +140,7 @@ def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, Bitrate in bits/s :param list can_filters: - A list of dictionaries each containing a "can_id" and a "can_mask". - - >>> [{"can_id": 0x11, "can_mask": 0x21}] + See :meth:`can.BusABC.set_filters`. :param bool log_errors: If True, communication errors will appear as CAN messages with @@ -136,6 +149,7 @@ def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, :raises can.interfaces.nican.NicanError: If starting communication fails + """ if nican is None: raise ImportError("The NI-CAN driver could not be loaded. " @@ -188,17 +202,17 @@ def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, self.handle = ctypes.c_ulong() nican.ncOpenObject(channel, ctypes.byref(self.handle)) - def recv(self, timeout=None): + super(NicanBus, self).__init__(channel=channel, + can_filters=can_filters, bitrate=bitrate, + log_errors=log_errors, **kwargs) + + def _recv_internal(self, timeout): """ - Read a message from NI-CAN. + Read a message from a NI-CAN bus. :param float timeout: Max time to wait in seconds or None if infinite - :returns: - The CAN message or None if timeout - :rtype: can.Message - :raises can.interfaces.nican.NicanError: If reception fails """ @@ -213,7 +227,7 @@ def recv(self, timeout=None): self.handle, NC_ST_READ_AVAIL, timeout, ctypes.byref(state)) except NicanError as e: if e.error_code == TIMEOUT_ERROR_CODE: - return None + return None, True else: raise @@ -235,7 +249,7 @@ def recv(self, timeout=None): arbitration_id=arb_id, dlc=dlc, data=raw_msg.data[:dlc]) - return msg + return msg, True def send(self, msg, timeout=None): """ @@ -258,6 +272,7 @@ def send(self, msg, timeout=None): nican.ncWrite( self.handle, ctypes.sizeof(raw_msg), ctypes.byref(raw_msg)) + # TODO: # ncWaitForState can not be called here if the recv() method is called # from a different thread, which is a very common use case. # Maybe it is possible to use ncCreateNotification instead but seems a @@ -276,6 +291,16 @@ def shutdown(self): """Close object.""" nican.ncCloseObject(self.handle) + __set_filters_has_been_called = False + def set_filters(self, can_filers=None): + """Unsupported. See note on :class:`~can.interfaces.nican.NicanBus`. + """ + if self.__set_filters_has_been_called: + logger.warn("using filters is not supported like this, see note on NicanBus") + else: + # allow the constructor to call this without causing a warning + self.__set_filters_has_been_called = True + class NicanError(CanError): """Error from NI-CAN driver.""" diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index abc352677..4f8e19729 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -5,15 +5,16 @@ Enable basic CAN over a PCAN USB device. """ +from __future__ import absolute_import, print_function + import logging import sys import time import can -from can import CanError -from can.bus import BusABC, BusState -from can.message import Message -from can.interfaces.pcan.PCANBasic import * +from can import CanError, Message, BusABC +from .PCANBasic import * +from can.bus import BusState boottimeEpoch = 0 try: @@ -71,10 +72,11 @@ def __init__(self, channel, state=BusState.ACTIVE, *args, **kwargs): """A PCAN USB interface to CAN. On top of the usual :class:`~can.Bus` methods provided, - the PCAN interface includes the `flash()` and `status()` methods. + the PCAN interface includes the :meth:`~can.interface.pcan.PcanBus.flash()` + and :meth:`~can.interface.pcan.PcanBus.status()` methods. :param str channel: - The can interface name. An example would be PCAN_USBBUS1 + The can interface name. An example would be 'PCAN_USBBUS1' :param BusState state: BusState of the channel. @@ -82,9 +84,10 @@ def __init__(self, channel, state=BusState.ACTIVE, *args, **kwargs): :param int bitrate: Bitrate of channel in bit/s. - Default is 500 Kbs + Default is 500 kbit/s. + """ - if channel is None or channel == '': + if not channel: raise ArgumentError("Must specify a PCAN channel") else: self.channel_info = channel @@ -116,18 +119,19 @@ def __init__(self, channel, state=BusState.ACTIVE, *args, **kwargs): if result != PCAN_ERROR_OK: raise PcanError(self._get_formatted_error(result)) - super(PcanBus, self).__init__(*args, **kwargs) + super(PcanBus, self).__init__(channel=channel, *args, **kwargs) def _get_formatted_error(self, error): """ - Gets the text using the GetErrorText API function - If the function succeeds, the translated error is returned. If it fails, - a text describing the current error is returned. Multiple errors may + Gets the text using the GetErrorText API function. + If the function call succeeds, the translated error is returned. If it fails, + a text describing the current error is returned. Multiple errors may be present in which case their individual messages are included in the return string, one line per error. """ def bits(n): + """TODO: document""" while n: b = n & (~n+1) yield b @@ -168,13 +172,14 @@ def status_is_ok(self): return status == PCAN_ERROR_OK def reset(self): - # Command the PCAN driver to reset the bus after an error. - + """ + Command the PCAN driver to reset the bus after an error. + """ status = self.m_objPCANBasic.Reset(self.m_PcanHandle) - return status == PCAN_ERROR_OK - def recv(self, timeout=None): + def _recv_internal(self, timeout): + if HAS_EVENTS: # We will utilize events for the timeout handling timeout_ms = int(timeout * 1000) if timeout is not None else INFINITE @@ -192,15 +197,15 @@ def recv(self, timeout=None): result = None val = WaitForSingleObject(self._recv_event, timeout_ms) if val != WAIT_OBJECT_0: - return None + return None, False elif timeout is not None and timeout_clock() >= end_time: - return None + return None, False else: result = None time.sleep(0.001) elif result[0] & (PCAN_ERROR_BUSLIGHT | PCAN_ERROR_BUSHEAVY): log.warning(self._get_formatted_error(result[0])) - return None + return None, False elif result[0] != PCAN_ERROR_OK: raise PcanError(self._get_formatted_error(result[0])) @@ -229,7 +234,7 @@ def recv(self, timeout=None): dlc=dlc, data=theMsg.DATA[:dlc]) - return rx_msg + return rx_msg, False def send(self, msg, timeout=None): if msg.id_type: @@ -290,4 +295,7 @@ def state(self, new_state): class PcanError(CanError): + """ + TODO: add docs + """ pass diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index b7c2f7c0e..0184d2abc 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -8,11 +8,12 @@ recording CAN traces. """ +from __future__ import absolute_import, division + import logging import struct -from can.bus import BusABC -from can.message import Message +from can import BusABC, Message logger = logging.getLogger('can.serial') @@ -27,32 +28,35 @@ class SerialBus(BusABC): """ Enable basic can communication over a serial device. + + .. note:: See :meth:`can.interfaces.serial.SerialBus._recv_internal` + for some special semantics. + """ - def __init__(self, channel, *args, **kwargs): + def __init__(self, channel, baudrate=115200, timeout=0.1, *args, **kwargs): """ :param str channel: The serial device to open. For example "/dev/ttyS1" or "/dev/ttyUSB0" on Linux or "COM1" on Windows systems. + :param int baudrate: Baud rate of the serial device in bit/s (default 115200). - .. note:: Some serial port implementations don't care about the baud - rate. + .. warning:: + Some serial port implementations don't care about the baudrate. :param float timeout: Timeout for the serial device in seconds (default 0.1). - """ - if channel == '': + """ + if not channel: raise ValueError("Must specify a serial port.") - else: - self.channel_info = "Serial interface: " + channel - baudrate = kwargs.get('baudrate', 115200) - timeout = kwargs.get('timeout', 0.1) - self.ser = serial.Serial(channel, baudrate=baudrate, - timeout=timeout) - super(SerialBus, self).__init__(*args, **kwargs) + + self.channel_info = "Serial interface: " + channel + self.ser = serial.Serial(channel, baudrate=baudrate, timeout=timeout) + + super(SerialBus, self).__init__(channel=channel, *args, **kwargs) def shutdown(self): """ @@ -67,24 +71,24 @@ def send(self, msg, timeout=None): :param can.Message msg: Message to send. - .. note:: Flags like extended_id, is_remote_frame and is_error_frame - will be ignored. + .. note:: Flags like ``extended_id``, ``is_remote_frame`` and + ``is_error_frame`` will be ignored. - .. note:: If the timestamp a float value it will be convert to an - integer. + .. note:: If the timestamp is a float value it will be converted + to an integer. :param timeout: This parameter will be ignored. The timeout value of the channel is - used. - """ + used instead. + """ try: - timestamp = struct.pack(' 0: - log.debug("Creating a filtered can bus") - self.set_filters(kwargs['can_filters']) - error = bindSocket(self.socket, channel) if error < 0: m = u'bindSocket failed for channel {} with error {}'.format( @@ -73,42 +69,38 @@ def __init__(self, if receive_own_messages: error1 = recv_own_msgs(self.socket) + # TODO handle potential error - super(SocketcanCtypes_Bus, self).__init__(*args, **kwargs) - - 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}] + self._is_filtered = False + kwargs.update({'receive_own_messages': receive_own_messages}) + super(SocketcanCtypes_Bus, self).__init__(channel=channel, *args, **kwargs) - A filter matches, when `` & can_mask == can_id & can_mask`` - - """ - filter_struct = pack_filters(can_filters) + def _apply_filters(self, filters): + filter_struct = pack_filters(filters) res = libc.setsockopt(self.socket, SOL_CAN_RAW, CAN_RAW_FILTER, - filter_struct, len(filter_struct) - ) - # TODO Is this serious enough to raise a CanError exception? + filter_struct, + len(filter_struct)) if res != 0: - log.error('Setting filters failed: ' + str(res)) + # fall back to "software filtering" (= not in kernel) + self._is_filtered = False + # TODO Is this serious enough to raise a CanError exception? + # TODO print error code (the errno, not "res", which is always -1) + log.error('Setting filters failed: falling back to software filtering (not in kernel)') + else: + self._is_filtered = True - def recv(self, timeout=None): + def _recv_internal(self, timeout): log.debug("Trying to read a msg") - if timeout is None or len(select.select([self.socket], - [], [], timeout)[0]) > 0: - packet = capturePacket(self.socket) - else: + ready_write_sockets, _, _ = select.select([self.socket], [], [], timeout) + if not ready_write_sockets: # socket wasn't readable or timeout occurred - return None + return None, self._is_filtered + + packet = capturePacket(self.socket) log.debug("Receiving a message") @@ -127,16 +119,15 @@ def recv(self, timeout=None): data=packet['Data'] ) - return rx_msg + return rx_msg, self._is_filtered def send(self, msg, timeout=None): frame = _build_can_frame(msg) - if timeout: - # Wait for write availability. write will fail below on timeout - _, ready_send_sockets, _ = select.select([], [self.socket], [], timeout) - if not ready_send_sockets: - raise can.CanError("Timeout while sending") + # Wait for write availability. write will fail below on timeout + _, ready_send_sockets, _ = select.select([], [self.socket], [], timeout) + if not ready_send_sockets: + raise can.CanError("Timeout while sending") # all sizes & lengths are in bytes total_sent = 0 diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 83c6874d2..6198493dc 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -402,28 +402,26 @@ def capture_message(sock): class SocketcanNative_Bus(BusABC): + """ + Implements :meth:`can.BusABC._detect_available_configs`. + """ 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'. + would be 'vcan0' or 'can0'. :param bool receive_own_messages: - If messages transmitted should also be received back. + If transmitted messages should also be received by this bus. :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". + See :meth:`can.BusABC.set_filters`. """ 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 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, @@ -433,34 +431,37 @@ def __init__(self, channel, receive_own_messages=False, fd=False, **kwargs): log.error("Could not receive own messages (%s)", e) if fd: + # TODO handle errors 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__() + + kwargs.update({'receive_own_messages': receive_own_messages, 'fd': fd}) + super(SocketcanNative_Bus, self).__init__(channel=channel, **kwargs) def shutdown(self): self.socket.close() - def recv(self, timeout=None): - try: - if timeout is not None: + def _recv_internal(self, timeout): + if timeout: + try: # 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 ready_receive_sockets: # not empty - return capture_message(self.socket) + except OSError: + # something bad happened (e.g. the interface went down) + log.exception("Error while waiting for timeout") + ready_receive_sockets = False + else: + ready_receive_sockets = True + + if ready_receive_sockets: # not empty or True + return capture_message(self.socket), self._is_filtered else: # socket wasn't readable or timeout occurred - return None + return None, self._is_filtered def send(self, msg, timeout=None): log.debug("We've been asked to write a message to the bus") @@ -488,11 +489,18 @@ def send_periodic(self, msg, period, duration=None): return task - 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) + def _apply_filters(self, filters): + try: + self.socket.setsockopt(socket.SOL_CAN_RAW, + socket.CAN_RAW_FILTER, + pack_filters(filters)) + except socket.error as err: + # fall back to "software filtering" (= not in kernel) + self._is_filtered = False + # TODO Is this serious enough to raise a CanError exception? + log.error('Setting filters failed; falling back to software filtering (not in kernel): %s', err) + else: + self._is_filtered = True @staticmethod def _detect_available_configs(): @@ -501,13 +509,16 @@ def _detect_available_configs(): if __name__ == "__main__": + # TODO move below to examples? + # Create two sockets on vcan0 to test send and receive # - # If you want to try it out you can do the following: + # If you want to try it out you can do the following (possibly using sudo): # # modprobe vcan # ip link add dev vcan0 type vcan # ifconfig vcan0 up + # log.setLevel(logging.DEBUG) def receiver(event): @@ -521,7 +532,8 @@ 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')) + msg = Message(arbitration_id=0x01, data=b'\x01\x02\x03') + sender_socket.send(build_can_frame(msg)) print("Sender sent a message.") import threading diff --git a/can/interfaces/usb2can/__init__.py b/can/interfaces/usb2can/__init__.py index 6cf4660f0..8262dc47b 100644 --- a/can/interfaces/usb2can/__init__.py +++ b/can/interfaces/usb2can/__init__.py @@ -4,5 +4,7 @@ """ """ -from can.interfaces.usb2can.usb2canInterface import Usb2canBus -from can.interfaces.usb2can.usb2canabstractionlayer import Usb2CanAbstractionLayer +from __future__ import absolute_import + +from .usb2canInterface import Usb2canBus +from .usb2canabstractionlayer import Usb2CanAbstractionLayer diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index 03f09ee8c..3a22ae5b4 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -5,10 +5,12 @@ This interface is for windows only, otherwise use socketCAN. """ +from __future__ import absolute_import, division + import logging from can import BusABC, Message -from can.interfaces.usb2can.usb2canabstractionlayer import * +from .usb2canabstractionlayer import * bootTimeEpoch = 0 try: @@ -42,7 +44,7 @@ def message_convert_tx(msg): for i in range(length): messagetx.data[i] = msg.data[i] - messagetx.flags = 80000000 + messagetx.flags = 0x80000000 if msg.is_error_frame: messagetx.flags |= IS_ERROR_FRAME @@ -77,8 +79,6 @@ def message_convert_rx(messagerx): class Usb2canBus(BusABC): """Interface to a USB2CAN Bus. - Note the USB2CAN interface doesn't implement set_filters, or flush_tx_buffer methods. - :param str channel: The device's serial number. If not provided, Windows Management Instrumentation will be used to identify the first such device. The *kwarg* `serial` may also be @@ -99,7 +99,6 @@ def __init__(self, channel, *args, **kwargs): # set flags on the connection if 'flags' in kwargs: enable_flags = kwargs["flags"] - else: enable_flags = 0x00000008 @@ -112,16 +111,10 @@ def __init__(self, channel, *args, **kwargs): from can.interfaces.usb2can.serial_selector import serial deviceID = serial() - # set baudrate in kb/s from bitrate - # (eg:500000 bitrate must be 500) - if 'bitrate' in kwargs: - br = kwargs["bitrate"] - - # max rate is 1000 kbps - baudrate = min(1000, int(br/1000)) - # set default value - else: - baudrate = 500 + # get baudrate in b/s from bitrate or use default + bitrate = kwargs.get("bitrate", d=500000) + # convert to kb/s (eg:500000 bitrate must be 500), max rate is 1000 kb/s + baudrate = min(1000, int(bitrate/1000)) connector = format_connection_string(deviceID, baudrate) @@ -134,7 +127,7 @@ def send(self, msg, timeout=None): else: self.can.send(self.handle, byref(tx)) - def recv(self, timeout=None): + def _recv_internal(self, timeout): messagerx = CanalMsg() @@ -154,8 +147,9 @@ def recv(self, timeout=None): log.error('Canal Error %s', status) rx = None - return rx + return rx, False def shutdown(self): """Shut down the device safely""" + # TODO handle error status = self.can.close(self.handle) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 9627c8fd1..7aecd9dd6 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -158,8 +158,6 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, else: LOG.info('Install pywin32 to avoid polling') - self.set_filters(can_filters) - try: vxlapi.xlActivateChannel(self.port_handle, self.mask, vxlapi.XL_BUS_TYPE_CAN, 0) @@ -172,26 +170,40 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, vxlapi.xlGetSyncTime(self.port_handle, offset) self._time_offset = time.time() - offset.value * 1e-9 - super(VectorBus, self).__init__() - - def set_filters(self, can_filters=None): - if can_filters: - # Only one filter per ID type allowed - if len(can_filters) == 1 or ( - len(can_filters) == 2 and - can_filters[0].get("extended") != can_filters[1].get("extended")): - for can_filter in can_filters: + self._is_filtered = False + super(VectorBus, self).__init__(channel=channel, can_filters=can_filters, + poll_interval=0.01, receive_own_messages=False, bitrate=None, + rx_queue_size=256, app_name="CANalyzer", **config) + + def _apply_filters(self, filters): + if filters: + # Only up to one filter per ID type allowed + if len(filters) == 1 or (len(filters) == 2 and + filters[0].get("extended") != filters[1].get("extended")): + for can_filter in filters: try: - vxlapi.xlCanSetChannelAcceptance( - self.port_handle, self.mask, + vxlapi.xlCanSetChannelAcceptance(self.port_handle, self.mask, can_filter["can_id"], can_filter["can_mask"], vxlapi.XL_CAN_EXT if can_filter.get("extended") else vxlapi.XL_CAN_STD) except VectorError as exc: LOG.warning("Could not set filters: %s", exc) + # go to fallback + else: + self._is_filtered = True + return else: - LOG.warning("Only one filter per extended or standard ID allowed") + LOG.warning("Only up to one filter per extended or standard ID allowed") + # go to fallback + + # fallback: reset filters + self._is_filtered = False + try: + vxlapi.xlCanSetChannelAcceptance(self.port_handle, self.mask, 0x0, 0x0, vxlapi.XL_CAN_EXT) + vxlapi.xlCanSetChannelAcceptance(self.port_handle, self.mask, 0x0, 0x0, vxlapi.XL_CAN_STD) + except VectorError as exc: + LOG.warning("Could not reset filters: %s", exc) - def recv(self, timeout=None): + def _recv_internal(self, timeout): end_time = time.time() + timeout if timeout is not None else None if self.fd: @@ -199,7 +211,7 @@ def recv(self, timeout=None): else: event = vxlapi.XLevent() event_count = ctypes.c_uint() - + while True: if self.fd: try: @@ -225,7 +237,7 @@ def recv(self, timeout=None): dlc=dlc, data=event.tagData.canRxOkMsg.data[:dlc], channel=event.chanIndex) - return msg + return msg, self._is_filtered else: event_count.value = 1 try: @@ -249,10 +261,10 @@ def recv(self, timeout=None): dlc=dlc, data=event.tagData.msg.data[:dlc], channel=event.chanIndex) - return msg + return msg, self._is_filtered if end_time is not None and time.time() > end_time: - return None + return None, self._is_filtered if HAS_EVENTS: # Wait for receive event to occur diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index c364cda59..46e393f54 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -16,7 +16,7 @@ except ImportError: import Queue as queue from threading import RLock -import random +from random import randint from can.bus import BusABC @@ -33,7 +33,7 @@ class VirtualBus(BusABC): A virtual CAN bus using an internal message queue. It can be used for example for testing. - In this interface, a channel is an arbitarty object used as + In this interface, a channel is an arbitrary object used as an identifier for connected buses. Implements :meth:`can.BusABC._detect_available_configs`; see @@ -42,6 +42,9 @@ class VirtualBus(BusABC): """ def __init__(self, channel=None, receive_own_messages=False, **config): + super(VirtualBus, self).__init__(channel=channel, + receive_own_messages=receive_own_messages, **config) + # the channel identifier may be an arbitrary object self.channel_id = channel self.channel_info = 'Virtual bus channel %s' % self.channel_id @@ -57,14 +60,14 @@ def __init__(self, channel=None, receive_own_messages=False, **config): self.queue = queue.Queue() self.channel.append(self.queue) - def recv(self, timeout=None): + def _recv_internal(self, timeout): try: msg = self.queue.get(block=True, timeout=timeout) except queue.Empty: - return None - - #logger.log(9, 'Received message:\n%s', msg) - return msg + return None, False + else: + #logger.log(9, 'Received message:\n%s', msg) + return msg, False def send(self, msg, timeout=None): msg.timestamp = time.time() @@ -78,7 +81,7 @@ def shutdown(self): with channels_lock: self.channel.remove(self.queue) - # remove if emtpy + # remove if empty if not self.channel: del channels[self.channel_id] @@ -88,14 +91,17 @@ def _detect_available_configs(): Returns all currently used channels as well as one other currently unused channel. - This method will have problems if thousands of - autodetected busses are used at once. + .. note:: + + This method will run into problems if thousands of + autodetected busses are used at once. + """ with channels_lock: available_channels = list(channels.keys()) # find a currently unused channel - get_extra = lambda: "channel-{}".format(random.randint(0, 9999)) + get_extra = lambda: "channel-{}".format(randint(0, 9999)) extra = get_extra() while extra in available_channels: extra = get_extra() diff --git a/can/util.py b/can/util.py index 6ca57b5ea..20c59bf0a 100644 --- a/can/util.py +++ b/can/util.py @@ -270,6 +270,6 @@ def dlc2len(dlc): if __name__ == "__main__": print("Searching for configuration named:") print("\n".join(CONFIG_FILES)) - + print() print("Settings:") print(load_config()) diff --git a/doc/development.rst b/doc/development.rst index c3ebb0541..1ff57c05a 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -27,6 +27,59 @@ The following assumes that the commands are executed from the root of the reposi makes Sphinx complain about more subtle problems. +Creating a new interface/backend +-------------------------------- + +These steps are a guideline on how to add a new backend to python-can. + +- Create a module (either a ``*.py`` or an entire subdirctory depending + on the complexity) inside ``can.interfaces`` +- Implement the central part of the backend: the bus class that extends + :class:`can.BusABC`. See below for more info on this one! +- Register your backend bus class in ``can.interface.BACKENDS`` and + ``can.interfaces.VALID_INTERFACES``. +- Add docs where appropiate, like in ``doc/interfaces.rst`` and add + an entry in ``doc/interface/*``. +- Add tests in ``test/*`` where appropiate. + +About the ``BusABC`` class +========================== + +Concrete implementations *have to* implement the following: + * :meth:`~can.BusABC.send` to send individual messages + * :meth:`~can.BusABC._recv_internal` to receive individual messages + (see note below!) + * set the :attr:`~can.BusABC.channel_info` attribute to a string describing + the underlying bus and/or channel + +They *might* implement the following: + * :meth:`~can.BusABC.flush_tx_buffer` to allow discrading any + messages yet to be sent + * :meth:`~can.BusABC.shutdown` to override how the bus should + shut down + * :meth:`~can.BusABC.send_periodic` to override the software based + periodic sending and push it down to the kernel or hardware + * :meth:`~can.BusABC._apply_filters` to apply efficient filters + to lower level systems like the OS kernel or hardware + * :meth:`~can.BusABC._detect_available_configs` to allow the interface + to report which configurations are currently available for new + connections + * :meth:`~can.BusABC.state` property to allow reading and/or changing + the bus state + +.. note:: + + *TL;DR*: Only override :meth:`~can.BusABC._recv_internal`, + never :meth:`~can.BusABC.recv` directly. + + Previously, concrete bus classes had to override :meth:`~can.BusABC.recv` + directly instead of :meth:`~can.BusABC._recv_internal`, but that has + changed to allow the abstract base class to handle in-software message + filtering as a fallback. All internal interfaces now implement that new + behaviour. Older (custom) interfaces might still be implemented like that + and thus might not provide message filtering: + + Creating a Release ------------------ @@ -41,7 +94,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, readthedocs and github. +- Check the release on PyPi, Read the Docs and GitHub. Code Structure diff --git a/setup.py b/setup.py index 59482bc74..0af5e8577 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ tests_require = [ 'mock >= 2.0.0', 'nose >= 1.3.7', + 'pytest-timeout >= 1.2.1', 'pyserial >= 3.0' ] diff --git a/test/data/example_data.py b/test/data/example_data.py index b84aa2c4e..c3290683a 100644 --- a/test/data/example_data.py +++ b/test/data/example_data.py @@ -106,6 +106,8 @@ ) ] +TEST_ALL_MESSAGES = TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + TEST_MESSAGES_ERROR_FRAMES + TEST_COMMENTS = [ "This is the first comment", "", # empty comment diff --git a/test/test_kvaser.py b/test/test_kvaser.py index eb0a73ac3..5bcd59103 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -39,7 +39,7 @@ def setUp(self): self.msg = {} self.msg_in_cue = None - self.bus = can.interface.Bus(channel=0, bustype='kvaser') + self.bus = can.Bus(channel=0, bustype='kvaser') def tearDown(self): if self.bus: @@ -123,7 +123,7 @@ def test_send_standard(self): self.assertSequenceEqual(self.msg['data'], [50, 51]) def test_recv_no_message(self): - self.assertEqual(self.bus.recv(), None) + self.assertEqual(self.bus.recv(timeout=0.5), None) def test_recv_extended(self): self.msg_in_cue = can.Message( diff --git a/test/test_message_filtering.py b/test/test_message_filtering.py new file mode 100644 index 000000000..278c92187 --- /dev/null +++ b/test/test_message_filtering.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module tests :meth:`can.BusABC._matches_filters`. +""" + +from __future__ import absolute_import + +import unittest + +from can import Bus, Message + +from .data.example_data import TEST_ALL_MESSAGES + + +EXAMPLE_MSG = Message(arbitration_id=0x123, extended_id=True) +HIGHEST_MSG = Message(arbitration_id=0x1FFFFFFF, extended_id=True) + +MATCH_EXAMPLE = [{ + "can_id": 0x123, + "can_mask": 0x1FFFFFFF, + "extended": True +}] + +MATCH_ONLY_HIGHEST = [{ + "can_id": 0xFFFFFFFF, + "can_mask": 0x1FFFFFFF, + "extended": True +}] + + +class TestMessageFiltering(unittest.TestCase): + + def setUp(self): + self.bus = Bus(bustype='virtual', channel='testy') + + def tearDown(self): + self.bus.shutdown() + + def test_match_all(self): + # explicitly + self.bus.set_filters() + self.assertTrue(self.bus._matches_filters(EXAMPLE_MSG)) + # implicitly + self.bus.set_filters(None) + self.assertTrue(self.bus._matches_filters(EXAMPLE_MSG)) + + def test_match_nothing(self): + self.bus.set_filters([]) + for msg in TEST_ALL_MESSAGES: + self.assertFalse(self.bus._matches_filters(msg)) + + def test_match_example_message(self): + self.bus.set_filters(MATCH_EXAMPLE) + self.assertTrue(self.bus._matches_filters(EXAMPLE_MSG)) + self.assertFalse(self.bus._matches_filters(HIGHEST_MSG)) + self.bus.set_filters(MATCH_ONLY_HIGHEST) + self.assertFalse(self.bus._matches_filters(EXAMPLE_MSG)) + self.assertTrue(self.bus._matches_filters(HIGHEST_MSG)) + + +if __name__ == '__main__': + unittest.main()