From f2cb4cbc29b198d65fa78b52fd69db75119e9f56 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Thu, 11 May 2023 23:12:02 +0200 Subject: [PATCH 01/46] ASCWriter: use correct channel for error frame (#1583) * use correct channel for error frame * add test --- can/io/asc.py | 15 ++++++++------- can/message.py | 4 +++- test/logformats_test.py | 22 +++++++++++++++++++++- test/message_helper.py | 31 ++++++------------------------- 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index b8054ecfc..5169d7468 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -420,8 +420,15 @@ def log_event(self, message: str, timestamp: Optional[float] = None) -> None: self.file.write(line) def on_message_received(self, msg: Message) -> None: + channel = channel2int(msg.channel) + if channel is None: + channel = self.channel + else: + # Many interfaces start channel numbering at 0 which is invalid + channel += 1 + if msg.is_error_frame: - self.log_event(f"{self.channel} ErrorFrame", msg.timestamp) + self.log_event(f"{channel} ErrorFrame", msg.timestamp) return if msg.is_remote_frame: dtype = f"r {msg.dlc:x}" # New after v8.5 @@ -432,12 +439,6 @@ def on_message_received(self, msg: Message) -> None: arb_id = f"{msg.arbitration_id:X}" if msg.is_extended_id: arb_id += "x" - channel = channel2int(msg.channel) - if channel is None: - channel = self.channel - else: - # Many interfaces start channel numbering at 0 which is invalid - channel += 1 if msg.is_fd: flags = 0 flags |= 1 << 12 diff --git a/can/message.py b/can/message.py index 05700ef72..02706583c 100644 --- a/can/message.py +++ b/can/message.py @@ -291,6 +291,7 @@ def equals( self, other: "Message", timestamp_delta: Optional[float] = 1.0e-6, + check_channel: bool = True, check_direction: bool = True, ) -> bool: """ @@ -299,6 +300,7 @@ def equals( :param other: the message to compare with :param timestamp_delta: the maximum difference in seconds at which two timestamps are still considered equal or `None` to not compare timestamps + :param check_channel: whether to compare the message channel :param check_direction: whether to compare the messages' directions (Tx/Rx) :return: True if and only if the given message equals this one @@ -322,7 +324,7 @@ def equals( and self.data == other.data and self.is_remote_frame == other.is_remote_frame and self.is_error_frame == other.is_error_frame - and self.channel == other.channel + and (self.channel == other.channel or not check_channel) and self.is_fd == other.is_fd and self.bitrate_switch == other.bitrate_switch and self.error_state_indicator == other.error_state_indicator diff --git a/test/logformats_test.py b/test/logformats_test.py index d6bd13b41..50f48c391 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -591,6 +591,26 @@ def test_no_triggerblock(self): def test_can_dlc_greater_than_8(self): _msg_list = self._read_log_file("issue_1299.asc") + def test_error_frame_channel(self): + # gh-issue 1578 + err_frame = can.Message(is_error_frame=True, channel=4) + + temp_file = tempfile.NamedTemporaryFile("w", delete=False) + temp_file.close() + + try: + with can.ASCWriter(temp_file.name) as writer: + writer.on_message_received(err_frame) + + with can.ASCReader(temp_file.name) as reader: + msg_list = list(reader) + assert len(msg_list) == 1 + assert err_frame.equals( + msg_list[0], check_channel=True + ), f"{err_frame!r}!={msg_list[0]!r}" + finally: + os.unlink(temp_file.name) + class TestBlfFileFormat(ReaderWriterTest): """Tests can.BLFWriter and can.BLFReader. @@ -814,7 +834,7 @@ def test_not_crashes_with_stdout(self): printer(message) def test_not_crashes_with_file(self): - with tempfile.NamedTemporaryFile("w", delete=False) as temp_file: + with tempfile.NamedTemporaryFile("w") as temp_file: with can.Printer(temp_file) as printer: for message in self.messages: printer(message) diff --git a/test/message_helper.py b/test/message_helper.py index fe193097b..d10e9195f 100644 --- a/test/message_helper.py +++ b/test/message_helper.py @@ -4,8 +4,6 @@ This module contains a helper for writing test cases that need to compare messages. """ -from copy import copy - class ComparingMessagesTestCase: """ @@ -28,31 +26,14 @@ def assertMessageEqual(self, message_1, message_2): Checks that two messages are equal, according to the given rules. """ - if message_1.equals(message_2, timestamp_delta=self.allowed_timestamp_delta): - return - elif self.preserves_channel: + if not message_1.equals( + message_2, + check_channel=self.preserves_channel, + timestamp_delta=self.allowed_timestamp_delta, + ): print(f"Comparing: message 1: {message_1!r}") print(f" message 2: {message_2!r}") - self.fail( - "messages are unequal with allowed timestamp delta {}".format( - self.allowed_timestamp_delta - ) - ) - else: - message_2 = copy(message_2) # make sure this method is pure - message_2.channel = message_1.channel - if message_1.equals( - message_2, timestamp_delta=self.allowed_timestamp_delta - ): - return - else: - print(f"Comparing: message 1: {message_1!r}") - print(f" message 2: {message_2!r}") - self.fail( - "messages are unequal with allowed timestamp delta {} even when ignoring channels".format( - self.allowed_timestamp_delta - ) - ) + self.fail(f"messages are unequal: \n{message_1}\n{message_2}") def assertMessagesEqual(self, messages_1, messages_2): """ From efb301ad62ff27787863356ebcef8421c712abda Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Thu, 11 May 2023 23:13:30 +0200 Subject: [PATCH 02/46] Fix PCAN OSError (#1580) --- can/interfaces/pcan/basic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index a60b96079..7c41e2816 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -690,13 +690,13 @@ def __init__(self): load_library_func = cdll.LoadLibrary if platform.system() == "Windows" or "CYGWIN" in platform.system(): - lib_name = "PCANBasic.dll" + lib_name = "PCANBasic" elif platform.system() == "Darwin": # PCBUSB library is a third-party software created # and maintained by the MacCAN project - lib_name = "libPCBUSB.dylib" + lib_name = "PCBUSB" else: - lib_name = "libpcanbasic.so" + lib_name = "pcanbasic" lib_path = find_library(lib_name) if not lib_path: From 1cfd87658e9b84796ffbec3527675cd26f9f6780 Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Fri, 12 May 2023 12:17:58 -0400 Subject: [PATCH 03/46] With Windows event the first two periodic frames are sent back without delay (#1590) When the ThreadBasedCyclicSendTask thread starts, the timer event was already set before entering the first loop. This resulted in the first timer wait to not wait. --- can/broadcastmanager.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 07d93e296..39dc1f0ba 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -293,6 +293,10 @@ def _run(self) -> None: msg_index = 0 msg_due_time_ns = time.perf_counter_ns() + if USE_WINDOWS_EVENTS: + # Make sure the timer is non-signaled before entering the loop + win32event.WaitForSingleObject(self.event.handle, 0) + while not self.stopped: # Prevent calling bus.send from multiple threads with self.send_lock: From 7eac6f723d660392ad522a6249f554c478725bdd Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Mon, 15 May 2023 11:49:52 +0200 Subject: [PATCH 04/46] update CHANGELOG.md for 4.2.1 (#1593) --- CHANGELOG.md | 10 ++++++++++ can/__init__.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66ce542ea..a240cb650 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +Version 4.2.1 +============= + +Bug Fixes +--------- +* The ASCWriter now logs the correct channel for error frames (#1578, #1583). +* Fix PCAN library detection (#1579, #1580). +* On Windows, the first two periodic frames were sent without delay (#1590). + + Version 4.2.0 ============= diff --git a/can/__init__.py b/can/__init__.py index 870bef73f..a14228f62 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging from typing import Any, Dict -__version__ = "4.2.0" +__version__ = "4.2.1" __all__ = [ "ASCReader", "ASCWriter", From 25fe56663d9b5798e21bd177192fb2e9f7893838 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Mon, 15 May 2023 16:19:01 +0200 Subject: [PATCH 05/46] Improve can.Bus typing (#1557) --- can/interface.py | 79 ++++++++++++++++++++-------------------- can/logger.py | 4 +-- can/util.py | 86 ++++++++++++++++++++++++-------------------- doc/bus.rst | 13 +++---- doc/conf.py | 4 ++- doc/internal-api.rst | 25 ++++++++----- 6 files changed, 114 insertions(+), 97 deletions(-) diff --git a/can/interface.py b/can/interface.py index 4c59dab8b..9c828a608 100644 --- a/can/interface.py +++ b/can/interface.py @@ -55,8 +55,20 @@ def _get_class_for_interface(interface: str) -> Type[BusABC]: return cast(Type[BusABC], bus_class) -class Bus(BusABC): # pylint: disable=abstract-method - """Bus wrapper with configuration loading. +@util.deprecated_args_alias( + deprecation_start="4.2.0", + deprecation_end="5.0.0", + bustype="interface", + context="config_context", +) +def Bus( + channel: Optional[Channel] = None, + interface: Optional[str] = None, + config_context: Optional[str] = None, + ignore_config: bool = False, + **kwargs: Any, +) -> BusABC: + """Create a new bus instance with configuration loading. Instantiates a CAN Bus of the given ``interface``, falls back to reading a configuration file from default locations. @@ -99,45 +111,30 @@ class Bus(BusABC): # pylint: disable=abstract-method if the ``channel`` could not be determined """ - @staticmethod - @util.deprecated_args_alias( - deprecation_start="4.2.0", - deprecation_end="5.0.0", - bustype="interface", - context="config_context", - ) - def __new__( # type: ignore - cls: Any, - channel: Optional[Channel] = None, - interface: Optional[str] = None, - config_context: Optional[str] = None, - ignore_config: bool = False, - **kwargs: Any, - ) -> BusABC: - # figure out the rest of the configuration; this might raise an error - if interface is not None: - kwargs["interface"] = interface - if channel is not None: - kwargs["channel"] = channel - - if not ignore_config: - kwargs = util.load_config(config=kwargs, context=config_context) - - # resolve the bus class to use for that interface - cls = _get_class_for_interface(kwargs["interface"]) - - # remove the "interface" key, so it doesn't get passed to the backend - del kwargs["interface"] - - # make sure the bus can handle this config format - channel = kwargs.pop("channel", channel) - if channel is None: - # Use the default channel for the backend - bus = cls(**kwargs) - else: - bus = cls(channel, **kwargs) + # figure out the rest of the configuration; this might raise an error + if interface is not None: + kwargs["interface"] = interface + if channel is not None: + kwargs["channel"] = channel + + if not ignore_config: + kwargs = util.load_config(config=kwargs, context=config_context) + + # resolve the bus class to use for that interface + cls = _get_class_for_interface(kwargs["interface"]) + + # remove the "interface" key, so it doesn't get passed to the backend + del kwargs["interface"] + + # make sure the bus can handle this config format + channel = kwargs.pop("channel", channel) + if channel is None: + # Use the default channel for the backend + bus = cls(**kwargs) + else: + bus = cls(channel, **kwargs) - return cast(BusABC, bus) + return bus def detect_available_configs( @@ -146,7 +143,7 @@ def detect_available_configs( """Detect all configurations/channels that the interfaces could currently connect with. - This might be quite time consuming. + This might be quite time-consuming. Automated configuration detection may not be implemented by every interface on every platform. This method will not raise diff --git a/can/logger.py b/can/logger.py index 42312324a..56f9156a8 100644 --- a/can/logger.py +++ b/can/logger.py @@ -85,7 +85,7 @@ def _append_filter_argument( ) -def _create_bus(parsed_args: Any, **kwargs: Any) -> can.Bus: +def _create_bus(parsed_args: Any, **kwargs: Any) -> can.BusABC: logging_level_names = ["critical", "error", "warning", "info", "debug", "subdebug"] can.set_logging_level(logging_level_names[min(5, parsed_args.verbosity)]) @@ -99,7 +99,7 @@ def _create_bus(parsed_args: Any, **kwargs: Any) -> can.Bus: if parsed_args.data_bitrate: config["data_bitrate"] = parsed_args.data_bitrate - return Bus(parsed_args.channel, **config) # type: ignore + return Bus(parsed_args.channel, **config) def _parse_filters(parsed_args: Any) -> CanFilters: diff --git a/can/util.py b/can/util.py index 2f6fb1957..59abdd579 100644 --- a/can/util.py +++ b/can/util.py @@ -24,6 +24,8 @@ cast, ) +from typing_extensions import ParamSpec + import can from . import typechecking @@ -325,9 +327,15 @@ def channel2int(channel: Optional[typechecking.Channel]) -> Optional[int]: return None -def deprecated_args_alias( # type: ignore - deprecation_start: str, deprecation_end: Optional[str] = None, **aliases -): +P1 = ParamSpec("P1") +T1 = TypeVar("T1") + + +def deprecated_args_alias( + deprecation_start: str, + deprecation_end: Optional[str] = None, + **aliases: Optional[str], +) -> Callable[[Callable[P1, T1]], Callable[P1, T1]]: """Allows to rename/deprecate a function kwarg(s) and optionally have the deprecated kwarg(s) set as alias(es) @@ -356,9 +364,9 @@ def library_function(new_arg): """ - def deco(f): + def deco(f: Callable[P1, T1]) -> Callable[P1, T1]: @functools.wraps(f) - def wrapper(*args, **kwargs): + def wrapper(*args: P1.args, **kwargs: P1.kwargs) -> T1: _rename_kwargs( func_name=f.__name__, start=deprecation_start, @@ -373,10 +381,42 @@ def wrapper(*args, **kwargs): return deco -T = TypeVar("T", BitTiming, BitTimingFd) +def _rename_kwargs( + func_name: str, + start: str, + end: Optional[str], + kwargs: P1.kwargs, + aliases: Dict[str, Optional[str]], +) -> None: + """Helper function for `deprecated_args_alias`""" + for alias, new in aliases.items(): + if alias in kwargs: + deprecation_notice = ( + f"The '{alias}' argument is deprecated since python-can v{start}" + ) + if end: + deprecation_notice += ( + f", and scheduled for removal in python-can v{end}" + ) + deprecation_notice += "." + + value = kwargs.pop(alias) + if new is not None: + deprecation_notice += f" Use '{new}' instead." + + if new in kwargs: + raise TypeError( + f"{func_name} received both '{alias}' (deprecated) and '{new}'." + ) + kwargs[new] = value + + warnings.warn(deprecation_notice, DeprecationWarning) + +T2 = TypeVar("T2", BitTiming, BitTimingFd) -def check_or_adjust_timing_clock(timing: T, valid_clocks: Iterable[int]) -> T: + +def check_or_adjust_timing_clock(timing: T2, valid_clocks: Iterable[int]) -> T2: """Adjusts the given timing instance to have an *f_clock* value that is within the allowed values specified by *valid_clocks*. If the *f_clock* value of timing is already within *valid_clocks*, then *timing* is returned unchanged. @@ -416,38 +456,6 @@ def check_or_adjust_timing_clock(timing: T, valid_clocks: Iterable[int]) -> T: ) from None -def _rename_kwargs( - func_name: str, - start: str, - end: Optional[str], - kwargs: Dict[str, str], - aliases: Dict[str, str], -) -> None: - """Helper function for `deprecated_args_alias`""" - for alias, new in aliases.items(): - if alias in kwargs: - deprecation_notice = ( - f"The '{alias}' argument is deprecated since python-can v{start}" - ) - if end: - deprecation_notice += ( - f", and scheduled for removal in python-can v{end}" - ) - deprecation_notice += "." - - value = kwargs.pop(alias) - if new is not None: - deprecation_notice += f" Use '{new}' instead." - - if new in kwargs: - raise TypeError( - f"{func_name} received both '{alias}' (deprecated) and '{new}'." - ) - kwargs[new] = value - - warnings.warn(deprecation_notice, DeprecationWarning) - - def time_perfcounter_correlation() -> Tuple[float, float]: """Get the `perf_counter` value nearest to when time.time() is updated diff --git a/doc/bus.rst b/doc/bus.rst index 51ed0220b..e21a9e5f1 100644 --- a/doc/bus.rst +++ b/doc/bus.rst @@ -3,10 +3,10 @@ Bus --- -The :class:`~can.Bus` provides a wrapper around a physical or virtual CAN Bus. +The :class:`~can.BusABC` class provides a wrapper around a physical or virtual CAN Bus. -An interface specific instance is created by instantiating the :class:`~can.Bus` -class with a particular ``interface``, for example:: +An interface specific instance is created by calling the :func:`~can.Bus` +function with a particular ``interface``, for example:: vector_bus = can.Bus(interface='vector', ...) @@ -77,13 +77,14 @@ See :meth:`~can.BusABC.set_filters` for the implementation. Bus API ''''''' -.. autoclass:: can.Bus +.. autofunction:: can.Bus + +.. autoclass:: can.BusABC :class-doc-from: class - :show-inheritance: :members: :inherited-members: -.. autoclass:: can.bus.BusState +.. autoclass:: can.BusState :members: :undoc-members: diff --git a/doc/conf.py b/doc/conf.py index aa61f243b..4b490ee29 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -126,7 +126,9 @@ ("py:class", "can.typechecking.CanFilter"), ("py:class", "can.typechecking.CanFilterExtended"), ("py:class", "can.typechecking.AutoDetectedConfig"), - ("py:class", "can.util.T"), + ("py:class", "can.util.T1"), + ("py:class", "can.util.T2"), + ("py:class", "~P1"), # intersphinx fails to reference some builtins ("py:class", "asyncio.events.AbstractEventLoop"), ("py:class", "_thread.allocate_lock"), diff --git a/doc/internal-api.rst b/doc/internal-api.rst index b8c108fb5..f4b6f875a 100644 --- a/doc/internal-api.rst +++ b/doc/internal-api.rst @@ -57,23 +57,32 @@ They **might** implement the following: and thus might not provide message filtering: -Concrete instances are usually created by :class:`can.Bus` which takes the users +Concrete instances are usually created by :func:`can.Bus` which takes the users configuration into account. Bus Internals ~~~~~~~~~~~~~ -Several methods are not documented in the main :class:`can.Bus` +Several methods are not documented in the main :class:`can.BusABC` as they are primarily useful for library developers as opposed to -library users. This is the entire ABC bus class with all internal -methods: +library users. -.. autoclass:: can.BusABC - :members: - :private-members: - :special-members: +.. automethod:: can.BusABC.__init__ + +.. automethod:: can.BusABC.__iter__ + +.. automethod:: can.BusABC.__str__ + +.. autoattribute:: can.BusABC.__weakref__ + +.. automethod:: can.BusABC._recv_internal + +.. automethod:: can.BusABC._apply_filters + +.. automethod:: can.BusABC._send_periodic_internal +.. automethod:: can.BusABC._detect_available_configs About the IO module From 09213b101adc1775b35dc57788d43902d6de85c9 Mon Sep 17 00:00:00 2001 From: Lukas Magel Date: Mon, 15 May 2023 16:21:47 +0200 Subject: [PATCH 06/46] Add `protocol` property to BusABC to determine active CAN Protocol (#1532) * Implement is_fd property for BusABC and PCANBus * Implement enum to represent CAN protocol * Implement CANProtocol for VirtualBus * Implement CANProtocol for UDPMulticastBus * Implement CANProtocol for the CANalystIIBus * Implement CANProtocol for the slcanBus * Rename CANProtocol to CanProtocol * Reimplement PcanBus.fd attribute as read-only property The property is scheduled for removal in v5.0 * Reimplement UdpMulticastBus.is_fd attribute as read-only property The property is superseded by BusABC.protocol and scheduled for removal in version 5.0. * Implement CanProtocol for robotellBus * Implement CanProtocol for NicanBus * Implement CanProtocol for IscanBus * Implement CanProtocol for CantactBus * Fix sphinx reference to CanProtocol * Implement CanProtocol for GsUsbBus * Implement CanProtocol for NiXNETcanBus * Implement CanProtocol for EtasBus * Implement CanProtocol for IXXATBus * Implement CanProtocol for KvaserBus * Implement CanProtocol for the SerialBus * Implement CanProtocol for UcanBus * Implement CanProtocol for VectorBus * Implement CanProtocol for NeousysBus * Implement CanProtocol for Usb2canBus * Implement CanProtocol for NeoViBus * Implement CanProtocol for SocketcanBus * Permit passthrough of protocol field for SocketCanDaemonBus * Implement CanProtocol for SeeedBus * Remove CanProtocol attribute from BusABC constructor The attribute is now set as class attribute with default value and can be overridden in the subclass constructor. * Apply suggestions from code review Fix property access and enum comparison Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> * Fix syntax error * Fix more enum comparisons against BusABC.protocol --------- Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> --- can/__init__.py | 3 +- can/bus.py | 18 +++++++++ can/interfaces/canalystii.py | 15 ++++--- can/interfaces/cantact.py | 8 +++- can/interfaces/etas/__init__.py | 8 ++-- can/interfaces/gs_usb.py | 7 +++- can/interfaces/ics_neovi/neovi_bus.py | 13 ++++-- can/interfaces/iscan.py | 7 +++- can/interfaces/ixxat/canlib.py | 3 ++ can/interfaces/ixxat/canlib_vcinpl.py | 3 +- can/interfaces/ixxat/canlib_vcinpl2.py | 10 ++--- can/interfaces/kvaser/canlib.py | 13 ++++-- can/interfaces/neousys/neousys.py | 8 ++-- can/interfaces/nican.py | 8 ++-- can/interfaces/nixnet.py | 19 +++++++-- can/interfaces/pcan/pcan.py | 29 +++++++++++--- can/interfaces/robotell.py | 3 +- can/interfaces/seeedstudio/seeedstudio.py | 4 +- can/interfaces/serial/serial_can.py | 2 + can/interfaces/slcan.py | 9 ++++- can/interfaces/socketcan/socketcan.py | 9 ++++- can/interfaces/socketcand/socketcand.py | 2 +- can/interfaces/systec/ucanbus.py | 19 +++++++-- can/interfaces/udp_multicast/bus.py | 34 +++++++++++----- can/interfaces/usb2can/usb2canInterface.py | 3 +- can/interfaces/vector/canlib.py | 26 +++++++++--- can/interfaces/virtual.py | 46 +++++++++++++++++++--- doc/bus.rst | 4 ++ test/serial_test.py | 3 ++ test/test_cantact.py | 5 +++ test/test_interface_canalystii.py | 7 ++++ test/test_kvaser.py | 7 +++- test/test_neousys.py | 3 ++ test/test_pcan.py | 15 +++++-- test/test_robotell.py | 3 ++ test/test_socketcan.py | 12 ++++++ test/test_systec.py | 2 + test/test_vector.py | 18 +++++++++ 38 files changed, 327 insertions(+), 81 deletions(-) diff --git a/can/__init__.py b/can/__init__.py index a14228f62..a6691eecb 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -25,6 +25,7 @@ "CanInitializationError", "CanInterfaceNotImplementedError", "CanOperationError", + "CanProtocol", "CanTimeoutError", "CanutilsLogReader", "CanutilsLogWriter", @@ -88,7 +89,7 @@ ModifiableCyclicTaskABC, RestartableCyclicTaskABC, ) -from .bus import BusABC, BusState +from .bus import BusABC, BusState, CanProtocol from .exceptions import ( CanError, CanInitializationError, diff --git a/can/bus.py b/can/bus.py index 5bd2d4e30..b7a54dbb1 100644 --- a/can/bus.py +++ b/can/bus.py @@ -26,6 +26,14 @@ class BusState(Enum): ERROR = auto() +class CanProtocol(Enum): + """The CAN protocol type supported by a :class:`can.BusABC` instance""" + + CAN_20 = auto() + CAN_FD = auto() + CAN_XL = auto() + + class BusABC(metaclass=ABCMeta): """The CAN Bus Abstract Base Class that serves as the basis for all concrete interfaces. @@ -44,6 +52,7 @@ class BusABC(metaclass=ABCMeta): RECV_LOGGING_LEVEL = 9 _is_shutdown: bool = False + _can_protocol: CanProtocol = CanProtocol.CAN_20 @abstractmethod def __init__( @@ -459,6 +468,15 @@ def state(self, new_state: BusState) -> None: """ raise NotImplementedError("Property is not implemented.") + @property + def protocol(self) -> CanProtocol: + """Return the CAN protocol used by this bus instance. + + This value is set at initialization time and does not change + during the lifetime of a bus instance. + """ + return self._can_protocol + @staticmethod def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: """Detect all configurations/channels that this interface could diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index dd65f5ba0..2fef19497 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -6,7 +6,7 @@ import canalystii as driver -from can import BitTiming, BitTimingFd, BusABC, Message +from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message from can.exceptions import CanTimeoutError from can.typechecking import CanFilters from can.util import check_or_adjust_timing_clock, deprecated_args_alias @@ -54,8 +54,11 @@ def __init__( raise ValueError("Either bitrate or timing argument is required") # Do this after the error handling - super().__init__(channel=channel, can_filters=can_filters, **kwargs) - + super().__init__( + channel=channel, + can_filters=can_filters, + **kwargs, + ) if isinstance(channel, str): # Assume comma separated string of channels self.channels = [int(ch.strip()) for ch in channel.split(",")] @@ -64,11 +67,11 @@ def __init__( else: # Sequence[int] self.channels = list(channel) - self.rx_queue: Deque[Tuple[int, driver.Message]] = deque(maxlen=rx_queue_size) - self.channel_info = f"CANalyst-II: device {device}, channels {self.channels}" - + self.rx_queue: Deque[Tuple[int, driver.Message]] = deque(maxlen=rx_queue_size) self.device = driver.CanalystDevice(device_index=device) + self._can_protocol = CanProtocol.CAN_20 + for single_channel in self.channels: if isinstance(timing, BitTiming): timing = check_or_adjust_timing_clock(timing, valid_clocks=[8_000_000]) diff --git a/can/interfaces/cantact.py b/can/interfaces/cantact.py index 75e1adbaa..963a9ee3b 100644 --- a/can/interfaces/cantact.py +++ b/can/interfaces/cantact.py @@ -7,7 +7,7 @@ from typing import Any, Optional, Union from unittest.mock import Mock -from can import BitTiming, BitTimingFd, BusABC, Message +from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message from ..exceptions import ( CanInitializationError, @@ -87,6 +87,7 @@ def __init__( self.channel = int(channel) self.channel_info = f"CANtact: ch:{channel}" + self._can_protocol = CanProtocol.CAN_20 # Configure the interface with error_check("Cannot setup the cantact.Interface", CanInitializationError): @@ -114,7 +115,10 @@ def __init__( self.interface.start() super().__init__( - channel=channel, bitrate=bitrate, poll_interval=poll_interval, **kwargs + channel=channel, + bitrate=bitrate, + poll_interval=poll_interval, + **kwargs, ) def _recv_internal(self, timeout): diff --git a/can/interfaces/etas/__init__.py b/can/interfaces/etas/__init__.py index 5f768b3e5..62e060f48 100644 --- a/can/interfaces/etas/__init__.py +++ b/can/interfaces/etas/__init__.py @@ -1,4 +1,3 @@ -import ctypes import time from typing import Dict, List, Optional, Tuple @@ -17,9 +16,12 @@ def __init__( bitrate: int = 1000000, fd: bool = True, data_bitrate: int = 2000000, - **kwargs: object, + **kwargs: Dict[str, any], ): + super().__init__(channel=channel, **kwargs) + self.receive_own_messages = receive_own_messages + self._can_protocol = can.CanProtocol.CAN_FD if fd else can.CanProtocol.CAN_20 nodeRange = CSI_NodeRange(CSI_NODE_MIN, CSI_NODE_MAX) self.tree = ctypes.POINTER(CSI_Tree)() @@ -297,7 +299,7 @@ def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: tree = ctypes.POINTER(CSI_Tree)() CSI_CreateProtocolTree(ctypes.c_char_p(b""), nodeRange, ctypes.byref(tree)) - nodes: Dict[str, str] = [] + nodes: List[Dict[str, str]] = [] def _findNodes(tree, prefix): uri = f"{prefix}/{tree.contents.item.uriName.decode()}" diff --git a/can/interfaces/gs_usb.py b/can/interfaces/gs_usb.py index fb5ce1d80..32ad54e75 100644 --- a/can/interfaces/gs_usb.py +++ b/can/interfaces/gs_usb.py @@ -52,11 +52,16 @@ def __init__( self.gs_usb = gs_usb self.channel_info = channel + self._can_protocol = can.CanProtocol.CAN_20 self.gs_usb.set_bitrate(bitrate) self.gs_usb.start() - super().__init__(channel=channel, can_filters=can_filters, **kwargs) + super().__init__( + channel=channel, + can_filters=can_filters, + **kwargs, + ) def send(self, msg: can.Message, timeout: Optional[float] = None): """Transmit a message to the CAN bus. diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 848cefcc8..f2dffe0a6 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -16,7 +16,7 @@ from threading import Event from warnings import warn -from can import BusABC, Message +from can import BusABC, CanProtocol, Message from ...exceptions import ( CanError, @@ -169,7 +169,11 @@ def __init__(self, channel, can_filters=None, **kwargs): if ics is None: raise ImportError("Please install python-ics") - super().__init__(channel=channel, can_filters=can_filters, **kwargs) + super().__init__( + channel=channel, + can_filters=can_filters, + **kwargs, + ) logger.info(f"CAN Filters: {can_filters}") logger.info(f"Got configuration of: {kwargs}") @@ -190,6 +194,9 @@ def __init__(self, channel, can_filters=None, **kwargs): serial = kwargs.get("serial") self.dev = self._find_device(type_filter, serial) + is_fd = kwargs.get("fd", False) + self._can_protocol = CanProtocol.CAN_FD if is_fd else CanProtocol.CAN_20 + with open_lock: ics.open_device(self.dev) @@ -198,7 +205,7 @@ def __init__(self, channel, can_filters=None, **kwargs): for channel in self.channels: ics.set_bit_rate(self.dev, kwargs.get("bitrate"), channel) - if kwargs.get("fd", False): + if is_fd: if "data_bitrate" in kwargs: for channel in self.channels: ics.set_fd_bit_rate( diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index ef3a48215..be0b0dae8 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -13,6 +13,7 @@ CanInitializationError, CanInterfaceNotImplementedError, CanOperationError, + CanProtocol, Message, ) @@ -99,6 +100,7 @@ def __init__( self.channel = ctypes.c_ubyte(int(channel)) self.channel_info = f"IS-CAN: {self.channel}" + self._can_protocol = CanProtocol.CAN_20 if bitrate not in self.BAUDRATES: raise ValueError(f"Invalid bitrate, choose one of {set(self.BAUDRATES)}") @@ -107,7 +109,10 @@ def __init__( iscan.isCAN_DeviceInitEx(self.channel, self.BAUDRATES[bitrate]) super().__init__( - channel=channel, bitrate=bitrate, poll_interval=poll_interval, **kwargs + channel=channel, + bitrate=bitrate, + poll_interval=poll_interval, + **kwargs, ) def _recv_internal( diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 3db719f96..b28c93541 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -131,6 +131,9 @@ def __init__( **kwargs ) + super().__init__(channel=channel, **kwargs) + self._can_protocol = self.bus.protocol + def flush_tx_buffer(self): """Flushes the transmit buffer on the IXXAT""" return self.bus.flush_tx_buffer() diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py index 550484f3e..5a366cc30 100644 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ b/can/interfaces/ixxat/canlib_vcinpl.py @@ -15,7 +15,7 @@ import sys from typing import Callable, Optional, Tuple -from can import BusABC, Message +from can import BusABC, CanProtocol, Message from can.broadcastmanager import ( LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC, @@ -490,6 +490,7 @@ def __init__( self._channel_capabilities = structures.CANCAPABILITIES() self._message = structures.CANMSG() self._payload = (ctypes.c_byte * 8)() + self._can_protocol = CanProtocol.CAN_20 # Search for supplied device if unique_hardware_id is None: diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index 3e7f2ff91..446b3e35c 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -15,8 +15,7 @@ import sys from typing import Callable, Optional, Tuple -import can.util -from can import BusABC, Message +from can import BusABC, CanProtocol, Message from can.broadcastmanager import ( LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC, @@ -24,7 +23,7 @@ from can.ctypesutil import HANDLE, PHANDLE, CLibrary from can.ctypesutil import HRESULT as ctypes_HRESULT from can.exceptions import CanInitializationError, CanInterfaceNotImplementedError -from can.util import deprecated_args_alias +from can.util import deprecated_args_alias, dlc2len, len2dlc from . import constants, structures from .exceptions import * @@ -536,6 +535,7 @@ def __init__( self._channel_capabilities = structures.CANCAPABILITIES2() self._message = structures.CANMSG2() self._payload = (ctypes.c_byte * 64)() + self._can_protocol = CanProtocol.CAN_FD # Search for supplied device if unique_hardware_id is None: @@ -865,7 +865,7 @@ def _recv_internal(self, timeout): # Timed out / can message type is not DATA return None, True - data_len = can.util.dlc2len(self._message.uMsgInfo.Bits.dlc) + data_len = dlc2len(self._message.uMsgInfo.Bits.dlc) # The _message.dwTime is a 32bit tick value and will overrun, # so expect to see the value restarting from 0 rx_msg = Message( @@ -915,7 +915,7 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: message.uMsgInfo.Bits.edl = 1 if msg.is_fd else 0 message.dwMsgId = msg.arbitration_id if msg.dlc: # this dlc means number of bytes of payload - message.uMsgInfo.Bits.dlc = can.util.len2dlc(msg.dlc) + message.uMsgInfo.Bits.dlc = len2dlc(msg.dlc) data_len_dif = msg.dlc - len(msg.data) data = msg.data + bytearray( [0] * data_len_dif diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index f6b92ccef..32d28059a 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -11,7 +11,7 @@ import sys import time -from can import BusABC, Message +from can import BusABC, CanProtocol, Message from can.util import time_perfcounter_correlation from ...exceptions import CanError, CanInitializationError, CanOperationError @@ -428,11 +428,12 @@ def __init__(self, channel, can_filters=None, **kwargs): channel = int(channel) except ValueError: raise ValueError("channel must be an integer") - self.channel = channel - log.debug("Initialising bus instance") + self.channel = channel self.single_handle = single_handle + self._can_protocol = CanProtocol.CAN_FD if fd else CanProtocol.CAN_20 + log.debug("Initialising bus instance") num_channels = ctypes.c_int(0) canGetNumberOfChannels(ctypes.byref(num_channels)) num_channels = int(num_channels.value) @@ -520,7 +521,11 @@ def __init__(self, channel, can_filters=None, **kwargs): self._timestamp_offset = time.time() - (timer.value * TIMESTAMP_FACTOR) self._is_filtered = False - super().__init__(channel=channel, can_filters=can_filters, **kwargs) + super().__init__( + channel=channel, + can_filters=can_filters, + **kwargs, + ) def _apply_filters(self, filters): if filters and len(filters) == 1: diff --git a/can/interfaces/neousys/neousys.py b/can/interfaces/neousys/neousys.py index d57234ddd..b7dd2117c 100644 --- a/can/interfaces/neousys/neousys.py +++ b/can/interfaces/neousys/neousys.py @@ -34,12 +34,13 @@ except ImportError: from ctypes import CDLL -from can import BusABC, Message - -from ...exceptions import ( +from can import ( + BusABC, CanInitializationError, CanInterfaceNotImplementedError, CanOperationError, + CanProtocol, + Message, ) logger = logging.getLogger(__name__) @@ -150,6 +151,7 @@ def __init__(self, channel, device=0, bitrate=500000, **kwargs): self.channel = channel self.device = device self.channel_info = f"Neousys Can: device {self.device}, channel {self.channel}" + self._can_protocol = CanProtocol.CAN_20 self.queue = queue.Queue() diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index 8457a1a38..f4b1a37f0 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -19,13 +19,14 @@ from typing import Optional, Tuple, Type import can.typechecking -from can import BusABC, Message - -from ..exceptions import ( +from can import ( + BusABC, CanError, CanInitializationError, CanInterfaceNotImplementedError, CanOperationError, + CanProtocol, + Message, ) logger = logging.getLogger(__name__) @@ -219,6 +220,7 @@ def __init__( self.channel = channel self.channel_info = f"NI-CAN: {channel}" + self._can_protocol = CanProtocol.CAN_20 channel_bytes = channel.encode("ascii") config = [(NC_ATTR_START_ON_OPEN, True), (NC_ATTR_LOG_COMM_ERRS, log_errors)] diff --git a/can/interfaces/nixnet.py b/can/interfaces/nixnet.py index 4ddb52455..ba665442e 100644 --- a/can/interfaces/nixnet.py +++ b/can/interfaces/nixnet.py @@ -11,12 +11,13 @@ import logging import os import time +import warnings from queue import SimpleQueue from types import ModuleType from typing import Any, List, Optional, Tuple, Union import can.typechecking -from can import BitTiming, BitTimingFd, BusABC, Message +from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message from can.exceptions import ( CanInitializationError, CanInterfaceNotImplementedError, @@ -103,10 +104,11 @@ def __init__( self.poll_interval = poll_interval - self.fd = isinstance(timing, BitTimingFd) if timing else fd + is_fd = isinstance(timing, BitTimingFd) if timing else fd + self._can_protocol = CanProtocol.CAN_FD if is_fd else CanProtocol.CAN_20 # Set database for the initialization - database_name = ":can_fd_brs:" if self.fd else ":memory:" + database_name = ":can_fd_brs:" if is_fd else ":memory:" try: # We need two sessions for this application, @@ -158,7 +160,7 @@ def __init__( if bitrate: self._interface.baud_rate = bitrate - if self.fd: + if is_fd: # See page 951 of NI-XNET Hardware and Software Manual # to set custom can configuration self._interface.can_fd_baud_rate = fd_bitrate or bitrate @@ -188,6 +190,15 @@ def __init__( **kwargs, ) + @property + def fd(self) -> bool: + warnings.warn( + "The NiXNETcanBus.fd property is deprecated and superseded by " + "BusABC.protocol. It is scheduled for removal in version 5.0.", + DeprecationWarning, + ) + return self._can_protocol is CanProtocol.CAN_FD + def _recv_internal( self, timeout: Optional[float] ) -> Tuple[Optional[Message], bool]: diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 61d19d1b4..a9b2c016b 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -4,6 +4,7 @@ import logging import platform import time +import warnings from datetime import datetime from typing import Any, List, Optional, Tuple, Union @@ -17,6 +18,7 @@ CanError, CanInitializationError, CanOperationError, + CanProtocol, Message, ) from can.util import check_or_adjust_timing_clock, dlc2len, len2dlc @@ -244,8 +246,9 @@ def __init__( err_msg = f"Cannot find a channel with ID {device_id:08x}" raise ValueError(err_msg) + is_fd = isinstance(timing, BitTimingFd) if timing else kwargs.get("fd", False) + self._can_protocol = CanProtocol.CAN_FD if is_fd else CanProtocol.CAN_20 self.channel_info = str(channel) - self.fd = isinstance(timing, BitTimingFd) if timing else kwargs.get("fd", False) hwtype = PCAN_TYPE_ISA ioport = 0x02A0 @@ -269,7 +272,7 @@ def __init__( result = self.m_objPCANBasic.Initialize( self.m_PcanHandle, pcan_bitrate, hwtype, ioport, interrupt ) - elif self.fd: + elif is_fd: if isinstance(timing, BitTimingFd): timing = check_or_adjust_timing_clock( timing, sorted(VALID_PCAN_FD_CLOCKS, reverse=True) @@ -336,7 +339,12 @@ def __init__( if result != PCAN_ERROR_OK: raise PcanCanInitializationError(self._get_formatted_error(result)) - super().__init__(channel=channel, state=state, bitrate=bitrate, **kwargs) + super().__init__( + channel=channel, + state=state, + bitrate=bitrate, + **kwargs, + ) def _find_channel_by_dev_id(self, device_id): """ @@ -482,7 +490,7 @@ def _recv_internal( end_time = time.time() + timeout if timeout is not None else None while True: - if self.fd: + if self._can_protocol is CanProtocol.CAN_FD: result, pcan_msg, pcan_timestamp = self.m_objPCANBasic.ReadFD( self.m_PcanHandle ) @@ -544,7 +552,7 @@ def _recv_internal( error_state_indicator = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_ESI.value) is_error_frame = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_ERRFRAME.value) - if self.fd: + if self._can_protocol is CanProtocol.CAN_FD: dlc = dlc2len(pcan_msg.DLC) timestamp = boottimeEpoch + (pcan_timestamp.value / (1000.0 * 1000.0)) else: @@ -590,7 +598,7 @@ def send(self, msg, timeout=None): if msg.error_state_indicator: msgType |= PCAN_MESSAGE_ESI.value - if self.fd: + if self._can_protocol is CanProtocol.CAN_FD: # create a TPCANMsg message structure CANMsg = TPCANMsgFD() @@ -649,6 +657,15 @@ def shutdown(self): self.m_objPCANBasic.Uninitialize(self.m_PcanHandle) + @property + def fd(self) -> bool: + warnings.warn( + "The PcanBus.fd property is deprecated and superseded by BusABC.protocol. " + "It is scheduled for removal in version 5.0.", + DeprecationWarning, + ) + return self._can_protocol is CanProtocol.CAN_FD + @property def state(self): return self._state diff --git a/can/interfaces/robotell.py b/can/interfaces/robotell.py index 4d038a38a..bfe8f5774 100644 --- a/can/interfaces/robotell.py +++ b/can/interfaces/robotell.py @@ -7,7 +7,7 @@ import time from typing import Optional -from can import BusABC, Message +from can import BusABC, CanProtocol, Message from ..exceptions import CanInterfaceNotImplementedError, CanOperationError @@ -92,6 +92,7 @@ def __init__( if bitrate is not None: self.set_bitrate(bitrate) + self._can_protocol = CanProtocol.CAN_20 self.channel_info = ( f"Robotell USB-CAN s/n {self.get_serial_number(1)} on {channel}" ) diff --git a/can/interfaces/seeedstudio/seeedstudio.py b/can/interfaces/seeedstudio/seeedstudio.py index 7d7a1e687..0540c78be 100644 --- a/can/interfaces/seeedstudio/seeedstudio.py +++ b/can/interfaces/seeedstudio/seeedstudio.py @@ -12,7 +12,7 @@ from time import time import can -from can import BusABC, Message +from can import BusABC, CanProtocol, Message logger = logging.getLogger("seeedbus") @@ -100,6 +100,8 @@ def __init__( self.op_mode = operation_mode self.filter_id = bytearray([0x00, 0x00, 0x00, 0x00]) self.mask_id = bytearray([0x00, 0x00, 0x00, 0x00]) + self._can_protocol = CanProtocol.CAN_20 + if not channel: raise can.CanInitializationError("Must specify a serial port.") diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index eb336feba..9de2da99c 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -17,6 +17,7 @@ CanInitializationError, CanInterfaceNotImplementedError, CanOperationError, + CanProtocol, CanTimeoutError, Message, ) @@ -88,6 +89,7 @@ def __init__( raise TypeError("Must specify a serial port.") self.channel_info = f"Serial interface: {channel}" + self._can_protocol = CanProtocol.CAN_20 try: self._ser = serial.serial_for_url( diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 21306f28f..7ff12ce44 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -7,7 +7,7 @@ import time from typing import Any, Optional, Tuple -from can import BusABC, Message, typechecking +from can import BusABC, CanProtocol, Message, typechecking from ..exceptions import ( CanInitializationError, @@ -105,6 +105,7 @@ def __init__( ) self._buffer = bytearray() + self._can_protocol = CanProtocol.CAN_20 time.sleep(sleep_after_open) @@ -118,7 +119,11 @@ def __init__( self.open() super().__init__( - channel, ttyBaudrate=115200, bitrate=None, rtscts=False, **kwargs + channel, + ttyBaudrate=115200, + bitrate=None, + rtscts=False, + **kwargs, ) def set_bitrate(self, bitrate: int) -> None: diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index bdf39f0ab..a3f74bb82 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -30,7 +30,7 @@ import can -from can import BusABC, Message +from can import BusABC, CanProtocol, Message from can.broadcastmanager import ( LimitedDurationCyclicSendTaskABC, ModifiableCyclicTaskABC, @@ -656,6 +656,7 @@ def __init__( self._is_filtered = False self._task_id = 0 self._task_id_guard = threading.Lock() + self._can_protocol = CanProtocol.CAN_FD if fd else CanProtocol.CAN_20 # set the local_loopback parameter try: @@ -710,7 +711,11 @@ def __init__( "local_loopback": local_loopback, } ) - super().__init__(channel=channel, can_filters=can_filters, **kwargs) + super().__init__( + channel=channel, + can_filters=can_filters, + **kwargs, + ) def shutdown(self) -> None: """Stops all active periodic tasks and closes the socket.""" diff --git a/can/interfaces/socketcand/socketcand.py b/can/interfaces/socketcand/socketcand.py index 0c6c06ccf..183a9ba12 100644 --- a/can/interfaces/socketcand/socketcand.py +++ b/can/interfaces/socketcand/socketcand.py @@ -93,7 +93,7 @@ def __init__(self, channel, host, port, can_filters=None, **kwargs): self._expect_msg("< ok >") self._tcp_send(f"< rawmode >") self._expect_msg("< ok >") - super().__init__(channel=channel, can_filters=can_filters) + super().__init__(channel=channel, can_filters=can_filters, **kwargs) def _recv_internal(self, timeout): if len(self.__message_buffer) != 0: diff --git a/can/interfaces/systec/ucanbus.py b/can/interfaces/systec/ucanbus.py index da05b38b1..cb0dcc39e 100644 --- a/can/interfaces/systec/ucanbus.py +++ b/can/interfaces/systec/ucanbus.py @@ -1,9 +1,16 @@ import logging from threading import Event -from can import BusABC, BusState, Message +from can import ( + BusABC, + BusState, + CanError, + CanInitializationError, + CanOperationError, + CanProtocol, + Message, +) -from ...exceptions import CanError, CanInitializationError, CanOperationError from .constants import * from .exceptions import UcanException from .structures import * @@ -104,6 +111,8 @@ def __init__(self, channel, can_filters=None, **kwargs): ) from exception self.channel = int(channel) + self._can_protocol = CanProtocol.CAN_20 + device_number = int(kwargs.get("device_number", ANY_MODULE)) # configuration options @@ -145,7 +154,11 @@ def __init__(self, channel, can_filters=None, **kwargs): self._is_filtered = False - super().__init__(channel=channel, can_filters=can_filters, **kwargs) + super().__init__( + channel=channel, + can_filters=can_filters, + **kwargs, + ) def _recv_internal(self, timeout): try: diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py index 00cbd32c8..089b8182f 100644 --- a/can/interfaces/udp_multicast/bus.py +++ b/can/interfaces/udp_multicast/bus.py @@ -3,21 +3,23 @@ import select import socket import struct +import warnings +from typing import List, Optional, Tuple, Union + +import can +from can import BusABC, CanProtocol +from can.typechecking import AutoDetectedConfig + +from .utils import check_msgpack_installed, pack_message, unpack_message try: from fcntl import ioctl except ModuleNotFoundError: # Missing on Windows pass -from typing import List, Optional, Tuple, Union log = logging.getLogger(__name__) -import can -from can import BusABC -from can.typechecking import AutoDetectedConfig - -from .utils import check_msgpack_installed, pack_message, unpack_message # see socket.getaddrinfo() IPv4_ADDRESS_INFO = Tuple[str, int] # address, port @@ -102,10 +104,22 @@ def __init__( "receiving own messages is not yet implemented" ) - super().__init__(channel, **kwargs) + super().__init__( + channel, + **kwargs, + ) - self.is_fd = fd self._multicast = GeneralPurposeUdpMulticastBus(channel, port, hop_limit) + self._can_protocol = CanProtocol.CAN_FD if fd else CanProtocol.CAN_20 + + @property + def is_fd(self) -> bool: + warnings.warn( + "The UdpMulticastBus.is_fd property is deprecated and superseded by " + "BusABC.protocol. It is scheduled for removal in version 5.0.", + DeprecationWarning, + ) + return self._can_protocol is CanProtocol.CAN_FD def _recv_internal(self, timeout: Optional[float]): result = self._multicast.recv(timeout) @@ -122,13 +136,13 @@ def _recv_internal(self, timeout: Optional[float]): "could not unpack received message" ) from exception - if not self.is_fd and can_message.is_fd: + if self._can_protocol is not CanProtocol.CAN_FD and can_message.is_fd: return None, False return can_message, False def send(self, msg: can.Message, timeout: Optional[float] = None) -> None: - if not self.is_fd and msg.is_fd: + if self._can_protocol is not CanProtocol.CAN_FD and msg.is_fd: raise can.CanOperationError( "cannot send FD message over bus with CAN FD disabled" ) diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index 2c0a0d00f..c89e394df 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -6,7 +6,7 @@ from ctypes import byref from typing import Optional -from can import BusABC, CanInitializationError, CanOperationError, Message +from can import BusABC, CanInitializationError, CanOperationError, CanProtocol, Message from .serial_selector import find_serial_devices from .usb2canabstractionlayer import ( @@ -118,6 +118,7 @@ def __init__( baudrate = min(int(bitrate // 1000), 1000) self.channel_info = f"USB2CAN device {device_id}" + self._can_protocol = CanProtocol.CAN_20 connector = f"{device_id}; {baudrate}" self.handle = self.can.open(connector, flags) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 03a3d9b1f..f852fb0ec 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -11,6 +11,7 @@ import logging import os import time +import warnings from types import ModuleType from typing import ( Any, @@ -44,6 +45,7 @@ BusABC, CanInitializationError, CanInterfaceNotImplementedError, + CanProtocol, Message, ) from can.typechecking import AutoDetectedConfig, CanFilters @@ -202,11 +204,12 @@ def __init__( ) channel_configs = get_channel_configs() + is_fd = isinstance(timing, BitTimingFd) if timing else fd self.mask = 0 - self.fd = isinstance(timing, BitTimingFd) if timing else fd self.channel_masks: Dict[int, int] = {} self.index_to_channel: Dict[int, int] = {} + self._can_protocol = CanProtocol.CAN_FD if is_fd else CanProtocol.CAN_20 for channel in self.channels: channel_index = self._find_global_channel_idx( @@ -229,7 +232,7 @@ def __init__( interface_version = ( xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4 - if self.fd + if is_fd else xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION ) @@ -324,7 +327,20 @@ def __init__( self._time_offset = 0.0 self._is_filtered = False - super().__init__(channel=channel, can_filters=can_filters, **kwargs) + super().__init__( + channel=channel, + can_filters=can_filters, + **kwargs, + ) + + @property + def fd(self) -> bool: + warnings.warn( + "The VectorBus.fd property is deprecated and superseded by " + "BusABC.protocol. It is scheduled for removal in version 5.0.", + DeprecationWarning, + ) + return self._can_protocol is CanProtocol.CAN_FD def _find_global_channel_idx( self, @@ -647,7 +663,7 @@ def _recv_internal( while True: try: - if self.fd: + if self._can_protocol is CanProtocol.CAN_FD: msg = self._recv_canfd() else: msg = self._recv_can() @@ -781,7 +797,7 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: def _send_sequence(self, msgs: Sequence[Message]) -> int: """Send messages and return number of successful transmissions.""" - if self.fd: + if self._can_protocol is CanProtocol.CAN_FD: return self._send_can_fd_msg_sequence(msgs) else: return self._send_can_msg_sequence(msgs) diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 3eaefc230..62ad0cfe3 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -15,7 +15,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple from can import CanOperationError -from can.bus import BusABC +from can.bus import BusABC, CanProtocol from can.message import Message from can.typechecking import AutoDetectedConfig @@ -33,7 +33,8 @@ class VirtualBus(BusABC): """ - A virtual CAN bus using an internal message queue. It can be used for example for testing. + A virtual CAN bus using an internal message queue. It can be used for + example for testing. In this interface, a channel is an arbitrary object used as an identifier for connected buses. @@ -48,9 +49,11 @@ class VirtualBus(BusABC): if a message is sent to 5 receivers with the timeout set to 1.0. .. warning:: - This interface guarantees reliable delivery and message ordering, but does *not* implement rate - limiting or ID arbitration/prioritization under high loads. Please refer to the section - :ref:`virtual_interfaces_doc` for more information on this and a comparison to alternatives. + This interface guarantees reliable delivery and message ordering, but + does *not* implement rate limiting or ID arbitration/prioritization + under high loads. Please refer to the section + :ref:`virtual_interfaces_doc` for more information on this and a + comparison to alternatives. """ def __init__( @@ -59,14 +62,45 @@ def __init__( receive_own_messages: bool = False, rx_queue_size: int = 0, preserve_timestamps: bool = False, + protocol: CanProtocol = CanProtocol.CAN_20, **kwargs: Any, ) -> None: + """ + The constructed instance has access to the bus identified by the + channel parameter. It is able to see all messages transmitted on the + bus by virtual instances constructed with the same channel identifier. + + :param channel: The channel identifier. This parameter can be an + arbitrary value. The bus instance will be able to see messages + from other virtual bus instances that were created with the same + value. + :param receive_own_messages: If set to True, sent messages will be + reflected back on the input queue. + :param rx_queue_size: The size of the reception queue. The reception + queue stores messages until they are read. If the queue reaches + its capacity, it will start dropping the oldest messages to make + room for new ones. If set to 0, the queue has an infinite capacity. + Be aware that this can cause memory leaks if messages are read + with a lower frequency than they arrive on the bus. + :param preserve_timestamps: If set to True, messages transmitted via + :func:`~can.BusABC.send` will keep the timestamp set in the + :class:`~can.Message` instance. Otherwise, the timestamp value + will be replaced with the current system time. + :param protocol: The protocol implemented by this bus instance. The + value does not affect the operation of the bus instance and can + be set to an arbitrary value for testing purposes. + :param kwargs: Additional keyword arguments passed to the parent + constructor. + """ super().__init__( - channel=channel, receive_own_messages=receive_own_messages, **kwargs + channel=channel, + receive_own_messages=receive_own_messages, + **kwargs, ) # the channel identifier may be an arbitrary object self.channel_id = channel + self._can_protocol = protocol self.channel_info = f"Virtual bus channel {self.channel_id}" self.receive_own_messages = receive_own_messages self.preserve_timestamps = preserve_timestamps diff --git a/doc/bus.rst b/doc/bus.rst index e21a9e5f1..f63c244c2 100644 --- a/doc/bus.rst +++ b/doc/bus.rst @@ -88,6 +88,10 @@ Bus API :members: :undoc-members: +.. autoclass:: can.bus.CanProtocol + :members: + :undoc-members: + Thread safe bus ''''''''''''''' diff --git a/test/serial_test.py b/test/serial_test.py index d020d7232..5fa90704b 100644 --- a/test/serial_test.py +++ b/test/serial_test.py @@ -50,6 +50,9 @@ def __init__(self): self, allowed_timestamp_delta=None, preserves_channel=True ) + def test_can_protocol(self): + self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20) + def test_rx_tx_min_max_data(self): """ Tests the transfer from 0x00 to 0xFF for a 1 byte payload diff --git a/test/test_cantact.py b/test/test_cantact.py index 2cc3e479c..f90655ae5 100644 --- a/test/test_cantact.py +++ b/test/test_cantact.py @@ -14,6 +14,8 @@ class CantactTest(unittest.TestCase): def test_bus_creation(self): bus = can.Bus(channel=0, interface="cantact", _testing=True) self.assertIsInstance(bus, cantact.CantactBus) + self.assertEqual(bus.protocol, can.CanProtocol.CAN_20) + cantact.MockInterface.set_bitrate.assert_called() cantact.MockInterface.set_bit_timing.assert_not_called() cantact.MockInterface.set_enabled.assert_called() @@ -25,7 +27,10 @@ def test_bus_creation_bittiming(self): bt = can.BitTiming(f_clock=24_000_000, brp=3, tseg1=13, tseg2=2, sjw=1) bus = can.Bus(channel=0, interface="cantact", timing=bt, _testing=True) + self.assertIsInstance(bus, cantact.CantactBus) + self.assertEqual(bus.protocol, can.CanProtocol.CAN_20) + cantact.MockInterface.set_bitrate.assert_not_called() cantact.MockInterface.set_bit_timing.assert_called() cantact.MockInterface.set_enabled.assert_called() diff --git a/test/test_interface_canalystii.py b/test/test_interface_canalystii.py index 0a87f40f9..65d9ee74b 100755 --- a/test/test_interface_canalystii.py +++ b/test/test_interface_canalystii.py @@ -22,6 +22,9 @@ def test_initialize_from_constructor(self): with create_mock_device() as mock_device: instance = mock_device.return_value bus = CANalystIIBus(bitrate=1000000) + + self.assertEqual(bus.protocol, can.CanProtocol.CAN_20) + instance.init.assert_has_calls( [ call(0, bitrate=1000000), @@ -34,6 +37,8 @@ def test_initialize_single_channel_only(self): with create_mock_device() as mock_device: instance = mock_device.return_value bus = CANalystIIBus(channel, bitrate=1000000) + + self.assertEqual(bus.protocol, can.CanProtocol.CAN_20) instance.init.assert_called_once_with(channel, bitrate=1000000) def test_initialize_with_timing_registers(self): @@ -43,6 +48,8 @@ def test_initialize_with_timing_registers(self): f_clock=8_000_000, btr0=0x03, btr1=0x6F ) bus = CANalystIIBus(bitrate=None, timing=timing) + self.assertEqual(bus.protocol, can.CanProtocol.CAN_20) + instance.init.assert_has_calls( [ call(0, timing0=0x03, timing1=0x6F), diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 6e7ccea38..043f86f8c 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -45,6 +45,7 @@ def tearDown(self): def test_bus_creation(self): self.assertIsInstance(self.bus, canlib.KvaserBus) + self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20) self.assertTrue(canlib.canOpenChannel.called) self.assertTrue(canlib.canBusOn.called) @@ -148,7 +149,8 @@ def test_available_configs(self): def test_canfd_default_data_bitrate(self): canlib.canSetBusParams.reset_mock() canlib.canSetBusParamsFd.reset_mock() - can.Bus(channel=0, interface="kvaser", fd=True) + bus = can.Bus(channel=0, interface="kvaser", fd=True) + self.assertEqual(bus.protocol, can.CanProtocol.CAN_FD) canlib.canSetBusParams.assert_called_once_with( 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0 ) @@ -160,7 +162,8 @@ def test_canfd_nondefault_data_bitrate(self): canlib.canSetBusParams.reset_mock() canlib.canSetBusParamsFd.reset_mock() data_bitrate = 2000000 - can.Bus(channel=0, interface="kvaser", fd=True, data_bitrate=data_bitrate) + bus = can.Bus(channel=0, interface="kvaser", fd=True, data_bitrate=data_bitrate) + self.assertEqual(bus.protocol, can.CanProtocol.CAN_FD) bitrate_constant = canlib.BITRATE_FD[data_bitrate] canlib.canSetBusParams.assert_called_once_with( 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0 diff --git a/test/test_neousys.py b/test/test_neousys.py index 080278d13..69c818869 100644 --- a/test/test_neousys.py +++ b/test/test_neousys.py @@ -36,6 +36,7 @@ def tearDown(self) -> None: def test_bus_creation(self) -> None: self.assertIsInstance(self.bus, neousys.NeousysBus) + self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20) neousys.NEOUSYS_CANLIB.CAN_Setup.assert_called() neousys.NEOUSYS_CANLIB.CAN_Start.assert_called() neousys.NEOUSYS_CANLIB.CAN_RegisterReceived.assert_called() @@ -62,6 +63,8 @@ def test_bus_creation(self) -> None: def test_bus_creation_bitrate(self) -> None: self.bus = can.Bus(channel=0, interface="neousys", bitrate=200000) self.assertIsInstance(self.bus, neousys.NeousysBus) + self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20) + CAN_Start_args = ( can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Setup.call_args[0] ) diff --git a/test/test_pcan.py b/test/test_pcan.py index 93a5f5ff4..19fa44dc7 100644 --- a/test/test_pcan.py +++ b/test/test_pcan.py @@ -11,7 +11,7 @@ from parameterized import parameterized import can -from can.bus import BusState +from can import BusState, CanProtocol from can.exceptions import CanInitializationError from can.interfaces.pcan import PcanBus, PcanError from can.interfaces.pcan.basic import * @@ -52,8 +52,10 @@ def test_bus_creation(self) -> None: self.bus = can.Bus(interface="pcan") self.assertIsInstance(self.bus, PcanBus) - self.MockPCANBasic.assert_called_once() + self.assertEqual(self.bus.protocol, CanProtocol.CAN_20) + self.assertFalse(self.bus.fd) + self.MockPCANBasic.assert_called_once() self.mock_pcan.Initialize.assert_called_once() self.mock_pcan.InitializeFD.assert_not_called() @@ -79,6 +81,9 @@ def test_bus_creation_fd(self, clock_param: str, clock_val: int) -> None: ) self.assertIsInstance(self.bus, PcanBus) + self.assertEqual(self.bus.protocol, CanProtocol.CAN_FD) + self.assertTrue(self.bus.fd) + self.MockPCANBasic.assert_called_once() self.mock_pcan.Initialize.assert_not_called() self.mock_pcan.InitializeFD.assert_called_once() @@ -451,10 +456,11 @@ def test_peak_fd_bus_constructor_regression(self): def test_constructor_bit_timing(self): timing = can.BitTiming.from_registers(f_clock=8_000_000, btr0=0x47, btr1=0x2F) - can.Bus(interface="pcan", channel="PCAN_USBBUS1", timing=timing) + bus = can.Bus(interface="pcan", channel="PCAN_USBBUS1", timing=timing) bitrate_arg = self.mock_pcan.Initialize.call_args[0][1] self.assertEqual(bitrate_arg.value, 0x472F) + self.assertEqual(bus.protocol, CanProtocol.CAN_20) def test_constructor_bit_timing_fd(self): timing = can.BitTimingFd( @@ -468,7 +474,8 @@ def test_constructor_bit_timing_fd(self): data_tseg2=6, data_sjw=1, ) - can.Bus(interface="pcan", channel="PCAN_USBBUS1", timing=timing) + bus = can.Bus(interface="pcan", channel="PCAN_USBBUS1", timing=timing) + self.assertEqual(bus.protocol, CanProtocol.CAN_FD) bitrate_arg = self.mock_pcan.InitializeFD.call_args[0][-1] diff --git a/test/test_robotell.py b/test/test_robotell.py index c0658ef2c..f95139917 100644 --- a/test/test_robotell.py +++ b/test/test_robotell.py @@ -15,6 +15,9 @@ def setUp(self): def tearDown(self): self.bus.shutdown() + def test_protocol(self): + self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20) + def test_recv_extended(self): self.serial.write( bytearray( diff --git a/test/test_socketcan.py b/test/test_socketcan.py index 90a143a36..f756cb93a 100644 --- a/test/test_socketcan.py +++ b/test/test_socketcan.py @@ -9,6 +9,8 @@ import warnings from unittest.mock import patch +from .config import TEST_INTERFACE_SOCKETCAN + import can from can.interfaces.socketcan.constants import ( CAN_BCM_TX_DELETE, @@ -357,6 +359,16 @@ def test_build_bcm_update_header(self): self.assertEqual(can_id, result.can_id) self.assertEqual(1, result.nframes) + @unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "Only run when vcan0 is available") + def test_bus_creation_can(self): + bus = can.Bus(interface="socketcan", channel="vcan0", fd=False) + self.assertEqual(bus.protocol, can.CanProtocol.CAN_20) + + @unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "Only run when vcan0 is available") + def test_bus_creation_can_fd(self): + bus = can.Bus(interface="socketcan", channel="vcan0", fd=True) + self.assertEqual(bus.protocol, can.CanProtocol.CAN_FD) + @unittest.skipUnless(IS_LINUX and IS_PYPY, "Only test when run on Linux with PyPy") def test_pypy_socketcan_support(self): """Wait for PyPy raw CAN socket support diff --git a/test/test_systec.py b/test/test_systec.py index 7495f75eb..86ed31362 100644 --- a/test/test_systec.py +++ b/test/test_systec.py @@ -36,6 +36,8 @@ def setUp(self): def test_bus_creation(self): self.assertIsInstance(self.bus, ucanbus.UcanBus) + self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20) + self.assertTrue(ucan.UcanInitHwConnectControlEx.called) self.assertTrue( ucan.UcanInitHardwareEx.called or ucan.UcanInitHardwareEx2.called diff --git a/test/test_vector.py b/test/test_vector.py index b6a0632a8..93aba9c7b 100644 --- a/test/test_vector.py +++ b/test/test_vector.py @@ -81,6 +81,8 @@ def mock_xldriver() -> None: def test_bus_creation_mocked(mock_xldriver) -> None: bus = can.Bus(channel=0, interface="vector", _testing=True) assert isinstance(bus, canlib.VectorBus) + assert bus.protocol == can.CanProtocol.CAN_20 + can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() @@ -97,6 +99,8 @@ def test_bus_creation_mocked(mock_xldriver) -> None: def test_bus_creation() -> None: bus = can.Bus(channel=0, serial=_find_virtual_can_serial(), interface="vector") assert isinstance(bus, canlib.VectorBus) + assert bus.protocol == can.CanProtocol.CAN_20 + bus.shutdown() xl_channel_config = _find_xl_channel_config( @@ -110,12 +114,15 @@ def test_bus_creation() -> None: bus = canlib.VectorBus(channel=0, serial=_find_virtual_can_serial()) assert isinstance(bus, canlib.VectorBus) + assert bus.protocol == can.CanProtocol.CAN_20 bus.shutdown() def test_bus_creation_bitrate_mocked(mock_xldriver) -> None: bus = can.Bus(channel=0, interface="vector", bitrate=200_000, _testing=True) assert isinstance(bus, canlib.VectorBus) + assert bus.protocol == can.CanProtocol.CAN_20 + can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() @@ -141,6 +148,7 @@ def test_bus_creation_bitrate() -> None: bitrate=200_000, ) assert isinstance(bus, canlib.VectorBus) + assert bus.protocol == can.CanProtocol.CAN_20 xl_channel_config = _find_xl_channel_config( serial=_find_virtual_can_serial(), channel=0 @@ -153,6 +161,8 @@ def test_bus_creation_bitrate() -> None: def test_bus_creation_fd_mocked(mock_xldriver) -> None: bus = can.Bus(channel=0, interface="vector", fd=True, _testing=True) assert isinstance(bus, canlib.VectorBus) + assert bus.protocol == can.CanProtocol.CAN_FD + can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() @@ -173,6 +183,7 @@ def test_bus_creation_fd() -> None: channel=0, serial=_find_virtual_can_serial(), interface="vector", fd=True ) assert isinstance(bus, canlib.VectorBus) + assert bus.protocol == can.CanProtocol.CAN_FD xl_channel_config = _find_xl_channel_config( serial=_find_virtual_can_serial(), channel=0 @@ -204,6 +215,8 @@ def test_bus_creation_fd_bitrate_timings_mocked(mock_xldriver) -> None: _testing=True, ) assert isinstance(bus, canlib.VectorBus) + assert bus.protocol == can.CanProtocol.CAN_FD + can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() @@ -346,6 +359,7 @@ def test_bus_creation_timing() -> None: timing=timing, ) assert isinstance(bus, canlib.VectorBus) + assert bus.protocol == can.CanProtocol.CAN_20 xl_channel_config = _find_xl_channel_config( serial=_find_virtual_can_serial(), channel=0 @@ -377,6 +391,8 @@ def test_bus_creation_timingfd_mocked(mock_xldriver) -> None: _testing=True, ) assert isinstance(bus, canlib.VectorBus) + assert bus.protocol == can.CanProtocol.CAN_FD + can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() @@ -425,6 +441,8 @@ def test_bus_creation_timingfd() -> None: timing=timing, ) + assert bus.protocol == can.CanProtocol.CAN_FD + xl_channel_config = _find_xl_channel_config( serial=_find_virtual_can_serial(), channel=0 ) From 791820953e7d409528e253c08364f35c822ed83c Mon Sep 17 00:00:00 2001 From: Alexander Bessman Date: Mon, 15 May 2023 17:37:43 +0200 Subject: [PATCH 07/46] Add auto-modifying cyclic tasks (#703) * Add auto-modifying cyclic tasks * Make sure self.modifier_callback exists * Don't break socketcan Added modifier_callback arguments where necessary, and changed MRO of sockercan's CyclicSendTask to match that of the fallback. * Remove __init__ from ModifiableCyclicTaskABC This makes several changes to socketcan in previous commits unnecessary. These changes are also removed. * Forgot some brackets... * Reformatting by black * modifier_callback should change one message per send * Forgot to change type hint * fix CI * use mutating callback, adapt SocketcanBus and ixxat, add test * improve docstring * fix ixxat imports --------- Co-authored-by: zariiii9003 Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> --- can/broadcastmanager.py | 32 ++++++++----- can/bus.py | 20 ++++++-- can/interfaces/ixxat/canlib.py | 22 +++++++-- can/interfaces/ixxat/canlib_vcinpl.py | 56 ++++++++++++++++------ can/interfaces/ixxat/canlib_vcinpl2.py | 56 ++++++++++++++++------ can/interfaces/socketcan/socketcan.py | 36 +++++++++++---- examples/cyclic_checksum.py | 64 ++++++++++++++++++++++++++ test/simplecyclic_test.py | 59 ++++++++++++++++++++---- 8 files changed, 274 insertions(+), 71 deletions(-) create mode 100644 examples/cyclic_checksum.py diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 39dc1f0ba..ae47fc048 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -53,7 +53,7 @@ def stop(self) -> None: """ -class CyclicSendTaskABC(CyclicTask): +class CyclicSendTaskABC(CyclicTask, abc.ABC): """ Message send task with defined period """ @@ -114,7 +114,7 @@ def _check_and_convert_messages( return messages -class LimitedDurationCyclicSendTaskABC(CyclicSendTaskABC): +class LimitedDurationCyclicSendTaskABC(CyclicSendTaskABC, abc.ABC): def __init__( self, messages: Union[Sequence[Message], Message], @@ -136,7 +136,7 @@ def __init__( self.duration = duration -class RestartableCyclicTaskABC(CyclicSendTaskABC): +class RestartableCyclicTaskABC(CyclicSendTaskABC, abc.ABC): """Adds support for restarting a stopped cyclic task""" @abc.abstractmethod @@ -144,9 +144,7 @@ def start(self) -> None: """Restart a stopped periodic task.""" -class ModifiableCyclicTaskABC(CyclicSendTaskABC): - """Adds support for modifying a periodic message""" - +class ModifiableCyclicTaskABC(CyclicSendTaskABC, abc.ABC): def _check_modified_messages(self, messages: Tuple[Message, ...]) -> None: """Helper function to perform error checking when modifying the data in the cyclic task. @@ -190,7 +188,7 @@ def modify_data(self, messages: Union[Sequence[Message], Message]) -> None: self.messages = messages -class MultiRateCyclicSendTaskABC(CyclicSendTaskABC): +class MultiRateCyclicSendTaskABC(CyclicSendTaskABC, abc.ABC): """A Cyclic send task that supports switches send frequency after a set time.""" def __init__( @@ -218,7 +216,7 @@ def __init__( class ThreadBasedCyclicSendTask( - ModifiableCyclicTaskABC, LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC + LimitedDurationCyclicSendTaskABC, ModifiableCyclicTaskABC, RestartableCyclicTaskABC ): """Fallback cyclic send task using daemon thread.""" @@ -230,6 +228,7 @@ def __init__( period: float, duration: Optional[float] = None, on_error: Optional[Callable[[Exception], bool]] = None, + modifier_callback: Optional[Callable[[Message], None]] = None, ) -> None: """Transmits `messages` with a `period` seconds for `duration` seconds on a `bus`. @@ -255,6 +254,7 @@ def __init__( time.perf_counter() + duration if duration else None ) self.on_error = on_error + self.modifier_callback = modifier_callback if USE_WINDOWS_EVENTS: self.period_ms = int(round(period * 1000, 0)) @@ -301,14 +301,22 @@ def _run(self) -> None: # Prevent calling bus.send from multiple threads with self.send_lock: try: + if self.modifier_callback is not None: + self.modifier_callback(self.messages[msg_index]) self.bus.send(self.messages[msg_index]) except Exception as exc: # pylint: disable=broad-except log.exception(exc) - if self.on_error: - if not self.on_error(exc): - break - else: + + # stop if `on_error` callback was not given + if self.on_error is None: + self.stop() + raise exc + + # stop if `on_error` returns False + if not self.on_error(exc): + self.stop() break + msg_due_time_ns += self.period_ns if self.end_time is not None and time.perf_counter() >= self.end_time: break diff --git a/can/bus.py b/can/bus.py index b7a54dbb1..9c65ad52f 100644 --- a/can/bus.py +++ b/can/bus.py @@ -8,7 +8,7 @@ from abc import ABC, ABCMeta, abstractmethod from enum import Enum, auto from time import time -from typing import Any, Iterator, List, Optional, Sequence, Tuple, Union, cast +from typing import Any, Callable, Iterator, List, Optional, Sequence, Tuple, Union, cast import can import can.typechecking @@ -195,6 +195,7 @@ def send_periodic( period: float, duration: Optional[float] = None, store_task: bool = True, + modifier_callback: Optional[Callable[[Message], None]] = None, ) -> can.broadcastmanager.CyclicSendTaskABC: """Start sending messages at a given period on this bus. @@ -216,6 +217,10 @@ def send_periodic( :param store_task: If True (the default) the task will be attached to this Bus instance. Disable to instead manage tasks manually. + :param modifier_callback: + Function which should be used to modify each message's data before + sending. The callback modifies the :attr:`~can.Message.data` of the + message and returns ``None``. :return: A started task instance. Note the task can be stopped (and depending on the backend modified) by calling the task's @@ -230,7 +235,7 @@ def send_periodic( .. note:: - For extremely long running Bus instances with many short lived + For extremely long-running Bus instances with many short-lived tasks the default api with ``store_task==True`` may not be appropriate as the stopped tasks are still taking up memory as they are associated with the Bus instance. @@ -247,9 +252,8 @@ def send_periodic( # Create a backend specific task; will be patched to a _SelfRemovingCyclicTask later task = cast( _SelfRemovingCyclicTask, - self._send_periodic_internal(msgs, period, duration), + self._send_periodic_internal(msgs, period, duration, modifier_callback), ) - # we wrap the task's stop method to also remove it from the Bus's list of tasks periodic_tasks = self._periodic_tasks original_stop_method = task.stop @@ -275,6 +279,7 @@ def _send_periodic_internal( msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, + modifier_callback: Optional[Callable[[Message], None]] = None, ) -> can.broadcastmanager.CyclicSendTaskABC: """Default implementation of periodic message sending using threading. @@ -298,7 +303,12 @@ def _send_periodic_internal( threading.Lock() ) task = ThreadBasedCyclicSendTask( - self, self._lock_send_periodic, msgs, period, duration + bus=self, + lock=self._lock_send_periodic, + messages=msgs, + period=period, + duration=duration, + modifier_callback=modifier_callback, ) return task diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index b28c93541..d47fc2d6a 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -1,9 +1,13 @@ -from typing import Optional +from typing import Callable, Optional, Sequence, Union import can.interfaces.ixxat.canlib_vcinpl as vcinpl import can.interfaces.ixxat.canlib_vcinpl2 as vcinpl2 -from can import BusABC, Message -from can.bus import BusState +from can import ( + BusABC, + BusState, + CyclicSendTaskABC, + Message, +) class IXXATBus(BusABC): @@ -145,8 +149,16 @@ def _recv_internal(self, timeout): def send(self, msg: Message, timeout: Optional[float] = None) -> None: return self.bus.send(msg, timeout) - def _send_periodic_internal(self, msgs, period, duration=None): - return self.bus._send_periodic_internal(msgs, period, duration) + def _send_periodic_internal( + self, + msgs: Union[Sequence[Message], Message], + period: float, + duration: Optional[float] = None, + modifier_callback: Optional[Callable[[Message], None]] = None, + ) -> CyclicSendTaskABC: + return self.bus._send_periodic_internal( + msgs, period, duration, modifier_callback + ) def shutdown(self) -> None: super().shutdown() diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py index 5a366cc30..cbf2fb61c 100644 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ b/can/interfaces/ixxat/canlib_vcinpl.py @@ -13,14 +13,18 @@ import functools import logging import sys -from typing import Callable, Optional, Tuple - -from can import BusABC, CanProtocol, Message -from can.broadcastmanager import ( +import warnings +from typing import Callable, Optional, Sequence, Tuple, Union + +from can import ( + BusABC, + BusState, + CanProtocol, + CyclicSendTaskABC, LimitedDurationCyclicSendTaskABC, + Message, RestartableCyclicTaskABC, ) -from can.bus import BusState from can.ctypesutil import HANDLE, PHANDLE, CLibrary from can.ctypesutil import HRESULT as ctypes_HRESULT from can.exceptions import CanInitializationError, CanInterfaceNotImplementedError @@ -785,17 +789,39 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: # Want to log outgoing messages? # log.log(self.RECV_LOGGING_LEVEL, "Sent: %s", message) - def _send_periodic_internal(self, msgs, period, duration=None): + def _send_periodic_internal( + self, + msgs: Union[Sequence[Message], Message], + period: float, + duration: Optional[float] = None, + modifier_callback: Optional[Callable[[Message], None]] = None, + ) -> CyclicSendTaskABC: """Send a message using built-in cyclic transmit list functionality.""" - if self._scheduler is None: - self._scheduler = HANDLE() - _canlib.canSchedulerOpen(self._device_handle, self.channel, self._scheduler) - caps = structures.CANCAPABILITIES() - _canlib.canSchedulerGetCaps(self._scheduler, caps) - self._scheduler_resolution = caps.dwClockFreq / caps.dwCmsDivisor - _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) - return CyclicSendTask( - self._scheduler, msgs, period, duration, self._scheduler_resolution + if modifier_callback is None: + if self._scheduler is None: + self._scheduler = HANDLE() + _canlib.canSchedulerOpen( + self._device_handle, self.channel, self._scheduler + ) + caps = structures.CANCAPABILITIES() + _canlib.canSchedulerGetCaps(self._scheduler, caps) + self._scheduler_resolution = caps.dwClockFreq / caps.dwCmsDivisor + _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) + return CyclicSendTask( + self._scheduler, msgs, period, duration, self._scheduler_resolution + ) + + # fallback to thread based cyclic task + warnings.warn( + f"{self.__class__.__name__} falls back to a thread-based cyclic task, " + "when the `modifier_callback` argument is given." + ) + return BusABC._send_periodic_internal( + self, + msgs=msgs, + period=period, + duration=duration, + modifier_callback=modifier_callback, ) def shutdown(self): diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index 446b3e35c..b796be744 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -13,11 +13,15 @@ import functools import logging import sys -from typing import Callable, Optional, Tuple +import warnings +from typing import Callable, Optional, Sequence, Tuple, Union -from can import BusABC, CanProtocol, Message -from can.broadcastmanager import ( +from can import ( + BusABC, + CanProtocol, + CyclicSendTaskABC, LimitedDurationCyclicSendTaskABC, + Message, RestartableCyclicTaskABC, ) from can.ctypesutil import HANDLE, PHANDLE, CLibrary @@ -931,19 +935,41 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: else: _canlib.canChannelPostMessage(self._channel_handle, message) - def _send_periodic_internal(self, msgs, period, duration=None): + def _send_periodic_internal( + self, + msgs: Union[Sequence[Message], Message], + period: float, + duration: Optional[float] = None, + modifier_callback: Optional[Callable[[Message], None]] = None, + ) -> CyclicSendTaskABC: """Send a message using built-in cyclic transmit list functionality.""" - if self._scheduler is None: - self._scheduler = HANDLE() - _canlib.canSchedulerOpen(self._device_handle, self.channel, self._scheduler) - caps = structures.CANCAPABILITIES2() - _canlib.canSchedulerGetCaps(self._scheduler, caps) - self._scheduler_resolution = ( - caps.dwCmsClkFreq / caps.dwCmsDivisor - ) # TODO: confirm - _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) - return CyclicSendTask( - self._scheduler, msgs, period, duration, self._scheduler_resolution + if modifier_callback is None: + if self._scheduler is None: + self._scheduler = HANDLE() + _canlib.canSchedulerOpen( + self._device_handle, self.channel, self._scheduler + ) + caps = structures.CANCAPABILITIES2() + _canlib.canSchedulerGetCaps(self._scheduler, caps) + self._scheduler_resolution = ( + caps.dwCmsClkFreq / caps.dwCmsDivisor + ) # TODO: confirm + _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) + return CyclicSendTask( + self._scheduler, msgs, period, duration, self._scheduler_resolution + ) + + # fallback to thread based cyclic task + warnings.warn( + f"{self.__class__.__name__} falls back to a thread-based cyclic task, " + "when the `modifier_callback` argument is given." + ) + return BusABC._send_periodic_internal( + self, + msgs=msgs, + period=period, + duration=duration, + modifier_callback=modifier_callback, ) def shutdown(self): diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index a3f74bb82..44cecec76 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -14,7 +14,8 @@ import struct import threading import time -from typing import Dict, List, Optional, Sequence, Tuple, Type, Union +import warnings +from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type, Union log = logging.getLogger(__name__) log_tx = log.getChild("tx") @@ -806,7 +807,8 @@ def _send_periodic_internal( msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, - ) -> CyclicSendTask: + modifier_callback: Optional[Callable[[Message], None]] = None, + ) -> can.broadcastmanager.CyclicSendTaskABC: """Start sending messages at a given period on this bus. The Linux kernel's Broadcast Manager SocketCAN API is used to schedule @@ -838,15 +840,29 @@ def _send_periodic_internal( general the message will be sent at the given rate until at least *duration* seconds. """ - msgs = LimitedDurationCyclicSendTaskABC._check_and_convert_messages( # pylint: disable=protected-access - msgs - ) + if modifier_callback is None: + msgs = LimitedDurationCyclicSendTaskABC._check_and_convert_messages( # pylint: disable=protected-access + msgs + ) + + msgs_channel = str(msgs[0].channel) if msgs[0].channel else None + bcm_socket = self._get_bcm_socket(msgs_channel or self.channel) + task_id = self._get_next_task_id() + task = CyclicSendTask(bcm_socket, task_id, msgs, period, duration) + return task - msgs_channel = str(msgs[0].channel) if msgs[0].channel else None - bcm_socket = self._get_bcm_socket(msgs_channel or self.channel) - task_id = self._get_next_task_id() - task = CyclicSendTask(bcm_socket, task_id, msgs, period, duration) - return task + # fallback to thread based cyclic task + warnings.warn( + f"{self.__class__.__name__} falls back to a thread-based cyclic task, " + "when the `modifier_callback` argument is given." + ) + return BusABC._send_periodic_internal( + self, + msgs=msgs, + period=period, + duration=duration, + modifier_callback=modifier_callback, + ) def _get_next_task_id(self) -> int: with self._task_id_guard: diff --git a/examples/cyclic_checksum.py b/examples/cyclic_checksum.py new file mode 100644 index 000000000..3ab6c78ac --- /dev/null +++ b/examples/cyclic_checksum.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +""" +This example demonstrates how to send a periodic message containing +an automatically updating counter and checksum. + +Expects a virtual interface: + + python3 -m examples.cyclic_checksum +""" + +import logging +import time + +import can + +logging.basicConfig(level=logging.INFO) + + +def cyclic_checksum_send(bus: can.BusABC) -> None: + """ + Sends periodic messages every 1 s with no explicit timeout. + The message's counter and checksum is updated before each send. + Sleeps for 10 seconds then stops the task. + """ + message = can.Message(arbitration_id=0x78, data=[0, 1, 2, 3, 4, 5, 6, 0]) + print("Starting to send an auto-updating message every 100ms for 3 s") + task = bus.send_periodic(msgs=message, period=0.1, modifier_callback=update_message) + time.sleep(3) + task.stop() + print("stopped cyclic send") + + +def update_message(message: can.Message) -> None: + counter = increment_counter(message) + checksum = compute_xbr_checksum(message, counter) + message.data[7] = (checksum << 4) + counter + + +def increment_counter(message: can.Message) -> int: + counter = message.data[7] & 0x0F + counter += 1 + counter %= 16 + + return counter + + +def compute_xbr_checksum(message: can.Message, counter: int) -> int: + """ + Computes an XBR checksum as per SAE J1939 SPN 3188. + """ + checksum = sum(message.data[:7]) + checksum += sum(message.arbitration_id.to_bytes(length=4, byteorder="big")) + checksum += counter & 0x0F + xbr_checksum = ((checksum >> 4) + checksum) & 0x0F + + return xbr_checksum + + +if __name__ == "__main__": + with can.Bus(channel=0, interface="virtual", receive_own_messages=True) as _bus: + notifier = can.Notifier(bus=_bus, listeners=[print]) + cyclic_checksum_send(_bus) + notifier.stop() diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 9e01be457..650a1fddf 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -5,8 +5,10 @@ """ import gc +import time import unittest from time import sleep +from typing import List from unittest.mock import MagicMock import can @@ -160,34 +162,73 @@ def test_thread_based_cyclic_send_task(self): # good case, bus is up on_error_mock = MagicMock(return_value=False) task = can.broadcastmanager.ThreadBasedCyclicSendTask( - bus, bus._lock_send_periodic, msg, 0.1, 3, on_error_mock + bus=bus, + lock=bus._lock_send_periodic, + messages=msg, + period=0.1, + duration=3, + on_error=on_error_mock, ) - task.start() sleep(1) on_error_mock.assert_not_called() task.stop() bus.shutdown() - # bus has been shutted down + # bus has been shut down on_error_mock = MagicMock(return_value=False) task = can.broadcastmanager.ThreadBasedCyclicSendTask( - bus, bus._lock_send_periodic, msg, 0.1, 3, on_error_mock + bus=bus, + lock=bus._lock_send_periodic, + messages=msg, + period=0.1, + duration=3, + on_error=on_error_mock, ) - task.start() sleep(1) - self.assertEqual(on_error_mock.call_count, 1) + self.assertEqual(1, on_error_mock.call_count) task.stop() - # bus is still shutted down, but on_error returns True + # bus is still shut down, but on_error returns True on_error_mock = MagicMock(return_value=True) task = can.broadcastmanager.ThreadBasedCyclicSendTask( - bus, bus._lock_send_periodic, msg, 0.1, 3, on_error_mock + bus=bus, + lock=bus._lock_send_periodic, + messages=msg, + period=0.1, + duration=3, + on_error=on_error_mock, ) - task.start() sleep(1) self.assertTrue(on_error_mock.call_count > 1) task.stop() + def test_modifier_callback(self) -> None: + msg_list: List[can.Message] = [] + + def increment_first_byte(msg: can.Message) -> None: + msg.data[0] += 1 + + original_msg = can.Message( + is_extended_id=False, arbitration_id=0x123, data=[0] * 8 + ) + + with can.ThreadSafeBus(interface="virtual", receive_own_messages=True) as bus: + notifier = can.Notifier(bus=bus, listeners=[msg_list.append]) + task = bus.send_periodic( + msgs=original_msg, period=0.001, modifier_callback=increment_first_byte + ) + time.sleep(0.2) + task.stop() + notifier.stop() + + self.assertEqual(b"\x01\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[0].data)) + self.assertEqual(b"\x02\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[1].data)) + self.assertEqual(b"\x03\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[2].data)) + self.assertEqual(b"\x04\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[3].data)) + self.assertEqual(b"\x05\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[4].data)) + self.assertEqual(b"\x06\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[5].data)) + self.assertEqual(b"\x07\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[6].data)) + if __name__ == "__main__": unittest.main() From 97f1e160f19e196871358920ba6d059f6b9663e3 Mon Sep 17 00:00:00 2001 From: Teejay Date: Tue, 16 May 2023 09:57:29 -0700 Subject: [PATCH 08/46] Convert setup.py to pyproject.toml (#1592) * Remove files * Update pyproject * Update ci * Fix ci errors * Apply formatter changes * Fix py312 ci * MR feedback Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> --------- Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> --- .github/workflows/ci.yml | 4 +- .github/workflows/format-code.yml | 2 +- can/interfaces/ixxat/canlib.py | 6 +- can/interfaces/seeedstudio/seeedstudio.py | 2 +- can/io/printer.py | 2 +- pyproject.toml | 143 ++++++++++++++++++++-- requirements-lint.txt | 6 - setup.cfg | 35 ------ setup.py | 104 ---------------- 9 files changed, 141 insertions(+), 163 deletions(-) delete mode 100644 requirements-lint.txt delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 949df6aab..3f143234b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,7 +84,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements-lint.txt + pip install -e .[lint] - name: mypy 3.7 run: | mypy --python-version 3.7 . @@ -125,7 +125,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements-lint.txt + pip install -e .[lint] - name: Code Format Check with Black run: | black --check --verbose . diff --git a/.github/workflows/format-code.yml b/.github/workflows/format-code.yml index 68c6f56d8..30f95c103 100644 --- a/.github/workflows/format-code.yml +++ b/.github/workflows/format-code.yml @@ -17,7 +17,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements-lint.txt + pip install -e .[lint] - name: Code Format Check with Black run: | black --verbose . diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index d47fc2d6a..8c07508e4 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -39,7 +39,7 @@ def __init__( tseg1_dbr: int = None, tseg2_dbr: int = None, ssp_dbr: int = None, - **kwargs + **kwargs, ): """ :param channel: @@ -116,7 +116,7 @@ def __init__( tseg1_dbr=tseg1_dbr, tseg2_dbr=tseg2_dbr, ssp_dbr=ssp_dbr, - **kwargs + **kwargs, ) else: if rx_fifo_size is None: @@ -132,7 +132,7 @@ def __init__( rx_fifo_size=rx_fifo_size, tx_fifo_size=tx_fifo_size, bitrate=bitrate, - **kwargs + **kwargs, ) super().__init__(channel=channel, **kwargs) diff --git a/can/interfaces/seeedstudio/seeedstudio.py b/can/interfaces/seeedstudio/seeedstudio.py index 0540c78be..8e0dca8c7 100644 --- a/can/interfaces/seeedstudio/seeedstudio.py +++ b/can/interfaces/seeedstudio/seeedstudio.py @@ -64,7 +64,7 @@ def __init__( operation_mode="normal", bitrate=500000, *args, - **kwargs + **kwargs, ): """ :param str channel: diff --git a/can/io/printer.py b/can/io/printer.py index d0df71db8..40b42862d 100644 --- a/can/io/printer.py +++ b/can/io/printer.py @@ -28,7 +28,7 @@ def __init__( self, file: Optional[Union[StringPathLike, TextIO]] = None, append: bool = False, - **kwargs: Any + **kwargs: Any, ) -> None: """ :param file: An optional path-like object or a file-like object to "print" diff --git a/pyproject.toml b/pyproject.toml index af952e51e..65b8f8aed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,19 +1,142 @@ [build-system] -requires = [ - "setuptools >= 40.8", - "wheel", -] +requires = ["setuptools >= 67.7.2"] build-backend = "setuptools.build_meta" +[project] +name = "python-can" +dynamic = ["readme", "version"] +description = "Controller Area Network interface module for Python" +authors = [{ name = "python-can contributors" }] +dependencies = [ + "wrapt~=1.10", + "packaging >= 23.1", + "setuptools >= 67.7.2", + "typing_extensions>=3.10.0.0", + "msgpack~=1.0.0; platform_system != 'Windows'", + "pywin32>=305; platform_system == 'Windows' and platform_python_implementation == 'CPython'", +] +requires-python = ">=3.7" +license = { text = "LGPL v3" } +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Information Technology", + "Intended Audience :: Manufacturing", + "Intended Audience :: Telecommunications Industry", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Natural Language :: English", + "Natural Language :: English", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: System :: Hardware :: Hardware Drivers", + "Topic :: System :: Logging", + "Topic :: System :: Monitoring", + "Topic :: System :: Networking", + "Topic :: Utilities", +] + +[project.scripts] +"can_logconvert.py" = "scripts.can_logconvert:main" +"can_logger.py" = "scripts.can_logger:main" +"can_player.py" = "scripts.can_player:main" +"can_viewer.py" = "scripts.can_viewer:main" + +[project.urls] +homepage = "https://github.com/hardbyte/python-can" +documentation = "https://python-can.readthedocs.io" +repository = "https://github.com/hardbyte/python-can" +changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" + +[project.optional-dependencies] +lint = [ + "pylint==2.16.4", + "ruff==0.0.260", + "black~=23.1.0", + "mypy==1.0.1", + "mypy-extensions==0.4.3", + "types-setuptools" +] +seeedstudio = ["pyserial>=3.0"] +serial = ["pyserial~=3.0"] +neovi = ["filelock", "python-ics>=2.12"] +canalystii = ["canalystii>=0.1.0"] +cantact = ["cantact>=0.0.7"] +cvector = ["python-can-cvector"] +gs_usb = ["gs_usb>=0.2.1"] +nixnet = ["nixnet>=0.3.2"] +pcan = ["uptime~=3.0.1"] +remote = ["python-can-remote"] +sontheim = ["python-can-sontheim>=0.1.2"] +canine = ["python-can-canine>=0.2.2"] +viewer = [ + "windows-curses; platform_system == 'Windows' and platform_python_implementation=='CPython'" +] +mf4 = ["asammdf>=6.0.0"] + +[tool.setuptools.dynamic] +readme = { file = "README.rst" } +version = { attr = "can.__version__" } + +[tool.setuptools.package-data] +"*" = ["README.rst", "CONTRIBUTORS.txt", "LICENSE.txt", "CHANGELOG.md"] +doc = ["*.*"] +examples = ["*.py"] +can = ["py.typed"] + +[tool.setuptools.packages.find] +include = ["can*", "scripts"] + +[tool.mypy] +warn_return_any = true +warn_unused_configs = true +ignore_missing_imports = true +no_implicit_optional = true +disallow_incomplete_defs = true +warn_redundant_casts = true +warn_unused_ignores = false +exclude = [ + "venv", + "^doc/conf.py$", + "^build", + "^test", + "^setup.py$", + "^can/interfaces/__init__.py", + "^can/interfaces/etas", + "^can/interfaces/gs_usb", + "^can/interfaces/ics_neovi", + "^can/interfaces/iscan", + "^can/interfaces/ixxat", + "^can/interfaces/kvaser", + "^can/interfaces/nican", + "^can/interfaces/neousys", + "^can/interfaces/pcan", + "^can/interfaces/serial", + "^can/interfaces/slcan", + "^can/interfaces/socketcan", + "^can/interfaces/systec", + "^can/interfaces/udp_multicast", + "^can/interfaces/usb2can", + "^can/interfaces/virtual", +] + [tool.ruff] select = [ - "F401", # unused-imports - "UP", # pyupgrade - "I", # isort -] + "F401", # unused-imports + "UP", # pyupgrade + "I", # isort -# Assume Python 3.7. -target-version = "py37" +] [tool.ruff.isort] known-first-party = ["can"] diff --git a/requirements-lint.txt b/requirements-lint.txt deleted file mode 100644 index 829fe5663..000000000 --- a/requirements-lint.txt +++ /dev/null @@ -1,6 +0,0 @@ -pylint==2.16.4 -ruff==0.0.260 -black~=23.1.0 -mypy==1.0.1 -mypy-extensions==0.4.3 -types-setuptools diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3e121b650..000000000 --- a/setup.cfg +++ /dev/null @@ -1,35 +0,0 @@ -[metadata] -license_files = LICENSE.txt - -[mypy] -warn_return_any = True -warn_unused_configs = True -ignore_missing_imports = True -no_implicit_optional = True -disallow_incomplete_defs = True -warn_redundant_casts = True -warn_unused_ignores = False -exclude = - (?x)( - venv - |^doc/conf.py$ - |^test - |^setup.py$ - |^can/interfaces/__init__.py - |^can/interfaces/etas - |^can/interfaces/gs_usb - |^can/interfaces/ics_neovi - |^can/interfaces/iscan - |^can/interfaces/ixxat - |^can/interfaces/kvaser - |^can/interfaces/nican - |^can/interfaces/neousys - |^can/interfaces/pcan - |^can/interfaces/serial - |^can/interfaces/slcan - |^can/interfaces/socketcan - |^can/interfaces/systec - |^can/interfaces/udp_multicast - |^can/interfaces/usb2can - |^can/interfaces/virtual - ) diff --git a/setup.py b/setup.py deleted file mode 100644 index 65298b072..000000000 --- a/setup.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python - -""" -Setup script for the `can` package. -Learn more at https://github.com/hardbyte/python-can/ -""" - -import logging -import re -from os import listdir -from os.path import isfile, join - -from setuptools import find_packages, setup - -logging.basicConfig(level=logging.WARNING) - -with open("can/__init__.py", encoding="utf-8") as fd: - version = re.search( - r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE - ).group(1) - -with open("README.rst", encoding="utf-8") as f: - long_description = f.read() - -# Dependencies -extras_require = { - "seeedstudio": ["pyserial>=3.0"], - "serial": ["pyserial~=3.0"], - "neovi": ["filelock", "python-ics>=2.12"], - "canalystii": ["canalystii>=0.1.0"], - "cantact": ["cantact>=0.0.7"], - "cvector": ["python-can-cvector"], - "gs_usb": ["gs_usb>=0.2.1"], - "nixnet": ["nixnet>=0.3.2"], - "pcan": ["uptime~=3.0.1"], - "remote": ["python-can-remote"], - "sontheim": ["python-can-sontheim>=0.1.2"], - "canine": ["python-can-canine>=0.2.2"], - "viewer": [ - 'windows-curses;platform_system=="Windows" and platform_python_implementation=="CPython"' - ], - "mf4": ["asammdf>=6.0.0"], -} - -setup( - # Description - name="python-can", - url="https://github.com/hardbyte/python-can", - description="Controller Area Network interface module for Python", - long_description=long_description, - long_description_content_type="text/x-rst", - classifiers=[ - # a list of all available ones: https://pypi.org/classifiers/ - "Programming Language :: Python", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Natural Language :: English", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", - "Operating System :: MacOS", - "Operating System :: POSIX :: Linux", - "Operating System :: Microsoft :: Windows", - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: Developers", - "Intended Audience :: Education", - "Intended Audience :: Information Technology", - "Intended Audience :: Manufacturing", - "Intended Audience :: Telecommunications Industry", - "Natural Language :: English", - "Topic :: System :: Logging", - "Topic :: System :: Monitoring", - "Topic :: System :: Networking", - "Topic :: System :: Hardware :: Hardware Drivers", - "Topic :: Utilities", - ], - version=version, - packages=find_packages(exclude=["test*", "doc", "scripts", "examples"]), - scripts=list(filter(isfile, (join("scripts/", f) for f in listdir("scripts/")))), - author="python-can contributors", - license="LGPL v3", - package_data={ - "": ["README.rst", "CONTRIBUTORS.txt", "LICENSE.txt", "CHANGELOG.md"], - "doc": ["*.*"], - "examples": ["*.py"], - "can": ["py.typed"], - }, - # Installation - # see https://www.python.org/dev/peps/pep-0345/#version-specifiers - python_requires=">=3.7", - install_requires=[ - "setuptools", - "wrapt~=1.10", - "typing_extensions>=3.10.0.0", - 'pywin32>=305;platform_system=="Windows" and platform_python_implementation=="CPython"', - 'msgpack~=1.0.0;platform_system!="Windows"', - "packaging", - ], - extras_require=extras_require, -) From b61482aa814c80b15527b17c83e3aece360e8d92 Mon Sep 17 00:00:00 2001 From: Robert Imschweiler <50044286+ro-i@users.noreply.github.com> Date: Tue, 16 May 2023 19:01:54 +0200 Subject: [PATCH 09/46] can.io: Distinguish Text/Binary-IO for Reader/Writer classes. (#1585) --- can/io/asc.py | 6 +++--- can/io/blf.py | 4 ++-- can/io/canutils.py | 6 +++--- can/io/csv.py | 6 +++--- can/io/generic.py | 20 ++++++++++++++++++++ can/io/logger.py | 34 +++++++++++++++++++++++++--------- can/io/mf4.py | 6 +++--- can/io/player.py | 23 ++++++++++++++++------- can/io/trc.py | 9 ++++++--- 9 files changed, 81 insertions(+), 33 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index 5169d7468..3114acfbe 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -14,7 +14,7 @@ from ..message import Message from ..typechecking import StringPathLike from ..util import channel2int, dlc2len, len2dlc -from .generic import FileIOMessageWriter, MessageReader +from .generic import TextIOMessageReader, TextIOMessageWriter CAN_MSG_EXT = 0x80000000 CAN_ID_MASK = 0x1FFFFFFF @@ -24,7 +24,7 @@ logger = logging.getLogger("can.io.asc") -class ASCReader(MessageReader): +class ASCReader(TextIOMessageReader): """ Iterator of CAN messages from a ASC logging file. Meta data (comments, bus statistics, J1939 Transport Protocol messages) is ignored. @@ -308,7 +308,7 @@ def __iter__(self) -> Generator[Message, None, None]: self.stop() -class ASCWriter(FileIOMessageWriter): +class ASCWriter(TextIOMessageWriter): """Logs CAN data to an ASCII log file (.asc). The measurement starts with the timestamp of the first registered message. diff --git a/can/io/blf.py b/can/io/blf.py index e9dd8380f..071c089d7 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -22,7 +22,7 @@ from ..message import Message from ..typechecking import StringPathLike from ..util import channel2int, dlc2len, len2dlc -from .generic import FileIOMessageWriter, MessageReader +from .generic import BinaryIOMessageReader, FileIOMessageWriter TSystemTime = Tuple[int, int, int, int, int, int, int, int] @@ -132,7 +132,7 @@ def systemtime_to_timestamp(systemtime: TSystemTime) -> float: return 0 -class BLFReader(MessageReader): +class BLFReader(BinaryIOMessageReader): """ Iterator of CAN messages from a Binary Logging File. diff --git a/can/io/canutils.py b/can/io/canutils.py index a9dced6a1..d7ae99daf 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -10,7 +10,7 @@ from can.message import Message from ..typechecking import StringPathLike -from .generic import FileIOMessageWriter, MessageReader +from .generic import TextIOMessageReader, TextIOMessageWriter log = logging.getLogger("can.io.canutils") @@ -23,7 +23,7 @@ CANFD_ESI = 0x02 -class CanutilsLogReader(MessageReader): +class CanutilsLogReader(TextIOMessageReader): """ Iterator over CAN messages from a .log Logging File (candump -L). @@ -122,7 +122,7 @@ def __iter__(self) -> Generator[Message, None, None]: self.stop() -class CanutilsLogWriter(FileIOMessageWriter): +class CanutilsLogWriter(TextIOMessageWriter): """Logs CAN data to an ASCII log file (.log). This class is is compatible with "candump -L". diff --git a/can/io/csv.py b/can/io/csv.py index b96e69342..2abaeb70e 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -15,10 +15,10 @@ from can.message import Message from ..typechecking import StringPathLike -from .generic import FileIOMessageWriter, MessageReader +from .generic import TextIOMessageReader, TextIOMessageWriter -class CSVReader(MessageReader): +class CSVReader(TextIOMessageReader): """Iterator over CAN messages from a .csv file that was generated by :class:`~can.CSVWriter` or that uses the same format as described there. Assumes that there is a header @@ -67,7 +67,7 @@ def __iter__(self) -> Generator[Message, None, None]: self.stop() -class CSVWriter(FileIOMessageWriter): +class CSVWriter(TextIOMessageWriter): """Writes a comma separated text file with a line for each message. Includes a header line. diff --git a/can/io/generic.py b/can/io/generic.py index 193ec3df2..eb9647474 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -1,13 +1,17 @@ """Contains generic base classes for file IO.""" +import gzip import locale from abc import ABCMeta from types import TracebackType from typing import ( Any, + BinaryIO, ContextManager, Iterable, Optional, + TextIO, Type, + Union, cast, ) @@ -105,5 +109,21 @@ def file_size(self) -> int: return self.file.tell() +class TextIOMessageWriter(FileIOMessageWriter, metaclass=ABCMeta): + file: TextIO + + +class BinaryIOMessageWriter(FileIOMessageWriter, metaclass=ABCMeta): + file: Union[BinaryIO, gzip.GzipFile] + + class MessageReader(BaseIOHandler, Iterable[Message], metaclass=ABCMeta): """The base class for all readers.""" + + +class TextIOMessageReader(MessageReader, metaclass=ABCMeta): + file: TextIO + + +class BinaryIOMessageReader(MessageReader, metaclass=ABCMeta): + file: Union[BinaryIO, gzip.GzipFile] diff --git a/can/io/logger.py b/can/io/logger.py index 07d288ba3..7075a7ee2 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -20,7 +20,12 @@ from .blf import BLFWriter from .canutils import CanutilsLogWriter from .csv import CSVWriter -from .generic import BaseIOHandler, FileIOMessageWriter, MessageWriter +from .generic import ( + BaseIOHandler, + BinaryIOMessageWriter, + FileIOMessageWriter, + MessageWriter, +) from .mf4 import MF4Writer from .printer import Printer from .sqlite import SqliteWriter @@ -94,20 +99,28 @@ def __new__( # type: ignore file_or_filename: AcceptedIOType = filename if suffix == ".gz": - suffix, file_or_filename = Logger.compress(filename, **kwargs) + LoggerType, file_or_filename = Logger.compress(filename, **kwargs) + else: + LoggerType = cls._get_logger_for_suffix(suffix) + + return LoggerType(file=file_or_filename, **kwargs) + @classmethod + def _get_logger_for_suffix(cls, suffix: str) -> Type[MessageWriter]: try: LoggerType = Logger.message_writers[suffix] if LoggerType is None: raise ValueError(f'failed to import logger for extension "{suffix}"') - return LoggerType(file=file_or_filename, **kwargs) + return LoggerType except KeyError: raise ValueError( f'No write support for this unknown log format "{suffix}"' ) from None - @staticmethod - def compress(filename: StringPathLike, **kwargs: Any) -> Tuple[str, FileLike]: + @classmethod + def compress( + cls, filename: StringPathLike, **kwargs: Any + ) -> Tuple[Type[MessageWriter], FileLike]: """ Return the suffix and io object of the decompressed file. File will automatically recompress upon close. @@ -117,12 +130,15 @@ def compress(filename: StringPathLike, **kwargs: Any) -> Tuple[str, FileLike]: raise ValueError( f"The file type {real_suffix} is currently incompatible with gzip." ) - if kwargs.get("append", False): - mode = "ab" if real_suffix == ".blf" else "at" + LoggerType = cls._get_logger_for_suffix(real_suffix) + append = kwargs.get("append", False) + + if issubclass(LoggerType, BinaryIOMessageWriter): + mode = "ab" if append else "wb" else: - mode = "wb" if real_suffix == ".blf" else "wt" + mode = "at" if append else "wt" - return real_suffix, gzip.open(filename, mode) + return LoggerType, gzip.open(filename, mode) def on_message_received(self, msg: Message) -> None: pass diff --git a/can/io/mf4.py b/can/io/mf4.py index faad9c37a..215543e9f 100644 --- a/can/io/mf4.py +++ b/can/io/mf4.py @@ -14,7 +14,7 @@ from ..message import Message from ..typechecking import StringPathLike from ..util import channel2int, dlc2len, len2dlc -from .generic import FileIOMessageWriter, MessageReader +from .generic import BinaryIOMessageReader, BinaryIOMessageWriter logger = logging.getLogger("can.io.mf4") @@ -75,7 +75,7 @@ CAN_ID_MASK = 0x1FFFFFFF -class MF4Writer(FileIOMessageWriter): +class MF4Writer(BinaryIOMessageWriter): """Logs CAN data to an ASAM Measurement Data File v4 (.mf4). MF4Writer does not support append mode. @@ -265,7 +265,7 @@ def on_message_received(self, msg: Message) -> None: self._rtr_buffer = np.zeros(1, dtype=RTR_DTYPE) -class MF4Reader(MessageReader): +class MF4Reader(BinaryIOMessageReader): """ Iterator of CAN messages from a MF4 logging file. diff --git a/can/io/player.py b/can/io/player.py index e4db0e167..5b9dc060a 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -16,7 +16,7 @@ from .blf import BLFReader from .canutils import CanutilsLogReader from .csv import CSVReader -from .generic import MessageReader +from .generic import BinaryIOMessageReader, MessageReader from .mf4 import MF4Reader from .sqlite import SqliteReader from .trc import TRCReader @@ -87,7 +87,13 @@ def __new__( # type: ignore file_or_filename: AcceptedIOType = filename if suffix == ".gz": - suffix, file_or_filename = LogReader.decompress(filename) + ReaderType, file_or_filename = LogReader.decompress(filename) + else: + ReaderType = cls._get_logger_for_suffix(suffix) + return ReaderType(file=file_or_filename, **kwargs) + + @classmethod + def _get_logger_for_suffix(cls, suffix: str) -> typing.Type[MessageReader]: try: ReaderType = LogReader.message_readers[suffix] except KeyError: @@ -96,19 +102,22 @@ def __new__( # type: ignore ) from None if ReaderType is None: raise ImportError(f"failed to import reader for extension {suffix}") - return ReaderType(file=file_or_filename, **kwargs) + return ReaderType - @staticmethod + @classmethod def decompress( + cls, filename: StringPathLike, - ) -> typing.Tuple[str, typing.Union[str, FileLike]]: + ) -> typing.Tuple[typing.Type[MessageReader], typing.Union[str, FileLike]]: """ Return the suffix and io object of the decompressed file. """ real_suffix = pathlib.Path(filename).suffixes[-2].lower() - mode = "rb" if real_suffix == ".blf" else "rt" + ReaderType = cls._get_logger_for_suffix(real_suffix) + + mode = "rb" if issubclass(ReaderType, BinaryIOMessageReader) else "rt" - return real_suffix, gzip.open(filename, mode) + return ReaderType, gzip.open(filename, mode) def __iter__(self) -> typing.Generator[Message, None, None]: raise NotImplementedError() diff --git a/can/io/trc.py b/can/io/trc.py index fc2a9e1f7..f116bdc04 100644 --- a/can/io/trc.py +++ b/can/io/trc.py @@ -16,7 +16,10 @@ from ..message import Message from ..typechecking import StringPathLike from ..util import channel2int, dlc2len, len2dlc -from .generic import FileIOMessageWriter, MessageReader +from .generic import ( + TextIOMessageReader, + TextIOMessageWriter, +) logger = logging.getLogger("can.io.trc") @@ -36,7 +39,7 @@ def __ge__(self, other): return NotImplemented -class TRCReader(MessageReader): +class TRCReader(TextIOMessageReader): """ Iterator of CAN messages from a TRC logging file. """ @@ -241,7 +244,7 @@ def __iter__(self) -> Generator[Message, None, None]: self.stop() -class TRCWriter(FileIOMessageWriter): +class TRCWriter(TextIOMessageWriter): """Logs CAN data to text file (.trc). The measurement starts with the timestamp of the first registered message. From 1a3f5e3769aa565ada8c27177a94f7db43d019dc Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 20 May 2023 14:41:00 +0200 Subject: [PATCH 10/46] Raise Minimum Python Version to 3.8 (#1597) * raise minimum python version to 3.8 * try to fix entry points * use walrus and hasattr * update supported versions --- .github/workflows/ci.yml | 5 ----- README.rst | 3 ++- can/__init__.py | 1 + can/_entry_points.py | 34 ++++++++++++++++++++++++++++++ can/broadcastmanager.py | 4 +--- can/bus.py | 25 +++++++++++++++++++--- can/interfaces/__init__.py | 43 +++++++++----------------------------- can/io/generic.py | 5 +++-- can/io/logger.py | 30 +++++++++++++------------- can/io/player.py | 43 +++++++++++++++++++------------------- can/notifier.py | 22 +++++++------------ can/thread_safe_bus.py | 21 ++----------------- can/typechecking.py | 12 +++++------ pyproject.toml | 19 +++++++---------- tox.ini | 4 ++-- 15 files changed, 133 insertions(+), 138 deletions(-) create mode 100644 can/_entry_points.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f143234b..9633398e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,12 +18,10 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] experimental: [false] python-version: [ - "3.7", "3.8", "3.9", "3.10", "3.11", - "pypy-3.7", "pypy-3.8", "pypy-3.9", ] @@ -85,9 +83,6 @@ jobs: run: | python -m pip install --upgrade pip pip install -e .[lint] - - name: mypy 3.7 - run: | - mypy --python-version 3.7 . - name: mypy 3.8 run: | mypy --python-version 3.8 . diff --git a/README.rst b/README.rst index e7d40f590..07d2b0668 100644 --- a/README.rst +++ b/README.rst @@ -62,7 +62,8 @@ Library Version Python ------------------------------ ----------- 2.x 2.6+, 3.4+ 3.x 2.7+, 3.5+ - 4.x 3.7+ + 4.0+ 3.7+ + 4.3+ 3.8+ ============================== =========== diff --git a/can/__init__.py b/can/__init__.py index a6691eecb..34db1727c 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -61,6 +61,7 @@ "exceptions", "interface", "interfaces", + "io", "listener", "logconvert", "log", diff --git a/can/_entry_points.py b/can/_entry_points.py new file mode 100644 index 000000000..6842e3c1a --- /dev/null +++ b/can/_entry_points.py @@ -0,0 +1,34 @@ +import importlib +import sys +from dataclasses import dataclass +from importlib.metadata import entry_points +from typing import Any, List + + +@dataclass +class _EntryPoint: + key: str + module_name: str + class_name: str + + def load(self) -> Any: + module = importlib.import_module(self.module_name) + return getattr(module, self.class_name) + + +# See https://docs.python.org/3/library/importlib.metadata.html#entry-points, +# "Compatibility Note". +if sys.version_info >= (3, 10): + + def read_entry_points(group: str) -> List[_EntryPoint]: + return [ + _EntryPoint(ep.name, ep.module, ep.attr) for ep in entry_points(group=group) + ] + +else: + + def read_entry_points(group: str) -> List[_EntryPoint]: + return [ + _EntryPoint(ep.name, *ep.value.split(":", maxsplit=1)) + for ep in entry_points().get(group, []) + ] diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index ae47fc048..84554a507 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -10,9 +10,7 @@ import sys import threading import time -from typing import TYPE_CHECKING, Callable, Optional, Sequence, Tuple, Union - -from typing_extensions import Final +from typing import TYPE_CHECKING, Callable, Final, Optional, Sequence, Tuple, Union from can import typechecking from can.message import Message diff --git a/can/bus.py b/can/bus.py index 9c65ad52f..555389b0f 100644 --- a/can/bus.py +++ b/can/bus.py @@ -8,7 +8,21 @@ from abc import ABC, ABCMeta, abstractmethod from enum import Enum, auto from time import time -from typing import Any, Callable, Iterator, List, Optional, Sequence, Tuple, Union, cast +from types import TracebackType +from typing import ( + Any, + Callable, + Iterator, + List, + Optional, + Sequence, + Tuple, + Type, + Union, + cast, +) + +from typing_extensions import Self import can import can.typechecking @@ -450,10 +464,15 @@ def shutdown(self) -> None: self._is_shutdown = True self.stop_all_periodic_tasks() - def __enter__(self): + def __enter__(self) -> Self: return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: self.shutdown() def __del__(self) -> None: diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index b14914230..f220d28e5 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -2,8 +2,9 @@ Interfaces contain low level implementations that interact with CAN hardware. """ -import sys -from typing import Dict, Tuple, cast +from typing import Dict, Tuple + +from can._entry_points import read_entry_points __all__ = [ "BACKENDS", @@ -60,36 +61,12 @@ "socketcand": ("can.interfaces.socketcand", "SocketCanDaemonBus"), } -if sys.version_info >= (3, 8): - from importlib.metadata import entry_points - - # See https://docs.python.org/3/library/importlib.metadata.html#entry-points, - # "Compatibility Note". - if sys.version_info >= (3, 10): - BACKENDS.update( - { - interface.name: (interface.module, interface.attr) - for interface in entry_points(group="can.interface") - } - ) - else: - # The entry_points().get(...) causes a deprecation warning on Python >= 3.10. - BACKENDS.update( - { - interface.name: cast( - Tuple[str, str], tuple(interface.value.split(":", maxsplit=1)) - ) - for interface in entry_points().get("can.interface", []) - } - ) -else: - from pkg_resources import iter_entry_points - BACKENDS.update( - { - interface.name: (interface.module_name, interface.attrs[0]) - for interface in iter_entry_points("can.interface") - } - ) +BACKENDS.update( + { + interface.key: (interface.module_name, interface.class_name) + for interface in read_entry_points(group="can.interface") + } +) -VALID_INTERFACES = frozenset(BACKENDS.keys()) +VALID_INTERFACES = frozenset(sorted(BACKENDS.keys())) diff --git a/can/io/generic.py b/can/io/generic.py index eb9647474..4d877865e 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -8,6 +8,7 @@ BinaryIO, ContextManager, Iterable, + Literal, Optional, TextIO, Type, @@ -15,7 +16,7 @@ cast, ) -from typing_extensions import Literal +from typing_extensions import Self from .. import typechecking from ..listener import Listener @@ -65,7 +66,7 @@ def __init__( # for multiple inheritance super().__init__() - def __enter__(self) -> "BaseIOHandler": + def __enter__(self) -> Self: return self def __exit__( diff --git a/can/io/logger.py b/can/io/logger.py index 7075a7ee2..c3e83c883 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -8,11 +8,11 @@ from abc import ABC, abstractmethod from datetime import datetime from types import TracebackType -from typing import Any, Callable, Dict, Optional, Set, Tuple, Type, cast +from typing import Any, Callable, Dict, Literal, Optional, Set, Tuple, Type, cast -from pkg_resources import iter_entry_points -from typing_extensions import Literal +from typing_extensions import Self +from .._entry_points import read_entry_points from ..listener import Listener from ..message import Message from ..typechecking import AcceptedIOType, FileLike, StringPathLike @@ -89,8 +89,8 @@ def __new__( # type: ignore if not Logger.fetched_plugins: Logger.message_writers.update( { - writer.name: writer.load() - for writer in iter_entry_points("can.io.message_writer") + writer.key: cast(Type[MessageWriter], writer.load()) + for writer in read_entry_points("can.io.message_writer") } ) Logger.fetched_plugins = True @@ -99,19 +99,19 @@ def __new__( # type: ignore file_or_filename: AcceptedIOType = filename if suffix == ".gz": - LoggerType, file_or_filename = Logger.compress(filename, **kwargs) + logger_type, file_or_filename = Logger.compress(filename, **kwargs) else: - LoggerType = cls._get_logger_for_suffix(suffix) + logger_type = cls._get_logger_for_suffix(suffix) - return LoggerType(file=file_or_filename, **kwargs) + return logger_type(file=file_or_filename, **kwargs) @classmethod def _get_logger_for_suffix(cls, suffix: str) -> Type[MessageWriter]: try: - LoggerType = Logger.message_writers[suffix] - if LoggerType is None: + logger_type = Logger.message_writers[suffix] + if logger_type is None: raise ValueError(f'failed to import logger for extension "{suffix}"') - return LoggerType + return logger_type except KeyError: raise ValueError( f'No write support for this unknown log format "{suffix}"' @@ -130,15 +130,15 @@ def compress( raise ValueError( f"The file type {real_suffix} is currently incompatible with gzip." ) - LoggerType = cls._get_logger_for_suffix(real_suffix) + logger_type = cls._get_logger_for_suffix(real_suffix) append = kwargs.get("append", False) - if issubclass(LoggerType, BinaryIOMessageWriter): + if issubclass(logger_type, BinaryIOMessageWriter): mode = "ab" if append else "wb" else: mode = "at" if append else "wt" - return LoggerType, gzip.open(filename, mode) + return logger_type, gzip.open(filename, mode) def on_message_received(self, msg: Message) -> None: pass @@ -275,7 +275,7 @@ def stop(self) -> None: """ self.writer.stop() - def __enter__(self) -> "BaseRotatingLogger": + def __enter__(self) -> Self: return self def __exit__( diff --git a/can/io/player.py b/can/io/player.py index 5b9dc060a..9fd9d9ed3 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -6,10 +6,9 @@ import gzip import pathlib import time -import typing - -from pkg_resources import iter_entry_points +from typing import Any, Dict, Generator, Iterable, Optional, Tuple, Type, Union, cast +from .._entry_points import read_entry_points from ..message import Message from ..typechecking import AcceptedIOType, FileLike, StringPathLike from .asc import ASCReader @@ -54,7 +53,7 @@ class LogReader(MessageReader): """ fetched_plugins = False - message_readers: typing.Dict[str, typing.Optional[typing.Type[MessageReader]]] = { + message_readers: Dict[str, Optional[Type[MessageReader]]] = { ".asc": ASCReader, ".blf": BLFReader, ".csv": CSVReader, @@ -66,9 +65,9 @@ class LogReader(MessageReader): @staticmethod def __new__( # type: ignore - cls: typing.Any, + cls: Any, filename: StringPathLike, - **kwargs: typing.Any, + **kwargs: Any, ) -> MessageReader: """ :param filename: the filename/path of the file to read from @@ -77,8 +76,8 @@ def __new__( # type: ignore if not LogReader.fetched_plugins: LogReader.message_readers.update( { - reader.name: reader.load() - for reader in iter_entry_points("can.io.message_reader") + reader.key: cast(Type[MessageReader], reader.load()) + for reader in read_entry_points("can.io.message_reader") } ) LogReader.fetched_plugins = True @@ -87,39 +86,39 @@ def __new__( # type: ignore file_or_filename: AcceptedIOType = filename if suffix == ".gz": - ReaderType, file_or_filename = LogReader.decompress(filename) + reader_type, file_or_filename = LogReader.decompress(filename) else: - ReaderType = cls._get_logger_for_suffix(suffix) - return ReaderType(file=file_or_filename, **kwargs) + reader_type = cls._get_logger_for_suffix(suffix) + return reader_type(file=file_or_filename, **kwargs) @classmethod - def _get_logger_for_suffix(cls, suffix: str) -> typing.Type[MessageReader]: + def _get_logger_for_suffix(cls, suffix: str) -> Type[MessageReader]: try: - ReaderType = LogReader.message_readers[suffix] + reader_type = LogReader.message_readers[suffix] except KeyError: raise ValueError( f'No read support for this unknown log format "{suffix}"' ) from None - if ReaderType is None: + if reader_type is None: raise ImportError(f"failed to import reader for extension {suffix}") - return ReaderType + return reader_type @classmethod def decompress( cls, filename: StringPathLike, - ) -> typing.Tuple[typing.Type[MessageReader], typing.Union[str, FileLike]]: + ) -> Tuple[Type[MessageReader], Union[str, FileLike]]: """ Return the suffix and io object of the decompressed file. """ real_suffix = pathlib.Path(filename).suffixes[-2].lower() - ReaderType = cls._get_logger_for_suffix(real_suffix) + reader_type = cls._get_logger_for_suffix(real_suffix) - mode = "rb" if issubclass(ReaderType, BinaryIOMessageReader) else "rt" + mode = "rb" if issubclass(reader_type, BinaryIOMessageReader) else "rt" - return ReaderType, gzip.open(filename, mode) + return reader_type, gzip.open(filename, mode) - def __iter__(self) -> typing.Generator[Message, None, None]: + def __iter__(self) -> Generator[Message, None, None]: raise NotImplementedError() @@ -130,7 +129,7 @@ class MessageSync: def __init__( self, - messages: typing.Iterable[Message], + messages: Iterable[Message], timestamps: bool = True, gap: float = 0.0001, skip: float = 60.0, @@ -148,7 +147,7 @@ def __init__( self.gap = gap self.skip = skip - def __iter__(self) -> typing.Generator[Message, None, None]: + def __iter__(self) -> Generator[Message, None, None]: t_wakeup = playback_start_time = time.perf_counter() recorded_start_time = None t_skipped = 0.0 diff --git a/can/notifier.py b/can/notifier.py index fce210f49..92a91f8a9 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -104,14 +104,13 @@ def stop(self, timeout: float = 5) -> None: # reader is a file descriptor self._loop.remove_reader(reader) for listener in self.listeners: - # Mypy prefers this over a hasattr(...) check - getattr(listener, "stop", lambda: None)() + if hasattr(listener, "stop"): + listener.stop() def _rx_thread(self, bus: BusABC) -> None: - msg = None try: while self._running: - if msg is not None: + if msg := bus.recv(self.timeout): with self._lock: if self._loop is not None: self._loop.call_soon_threadsafe( @@ -119,12 +118,11 @@ def _rx_thread(self, bus: BusABC) -> None: ) else: self._on_message_received(msg) - msg = bus.recv(self.timeout) except Exception as exc: # pylint: disable=broad-except self.exception = exc if self._loop is not None: self._loop.call_soon_threadsafe(self._on_error, exc) - # Raise anyways + # Raise anyway raise elif not self._on_error(exc): # If it was not handled, raise the exception here @@ -134,14 +132,13 @@ def _rx_thread(self, bus: BusABC) -> None: logger.info("suppressed exception: %s", exc) def _on_message_available(self, bus: BusABC) -> None: - msg = bus.recv(0) - if msg is not None: + if msg := bus.recv(0): self._on_message_received(msg) def _on_message_received(self, msg: Message) -> None: for callback in self.listeners: res = callback(msg) - if res is not None and self._loop is not None and asyncio.iscoroutine(res): + if res and self._loop and asyncio.iscoroutine(res): # Schedule coroutine self._loop.create_task(res) @@ -153,12 +150,9 @@ def _on_error(self, exc: Exception) -> bool: was_handled = False for listener in self.listeners: - on_error = getattr( - listener, "on_error", None - ) # Mypy prefers this over hasattr(...) - if on_error is not None: + if hasattr(listener, "on_error"): try: - on_error(exc) + listener.on_error(exc) except NotImplementedError: pass else: diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 4793ed1ff..9b008667f 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -10,26 +10,9 @@ ObjectProxy = object import_exc = exc -from .interface import Bus - -try: - from contextlib import nullcontext - -except ImportError: +from contextlib import nullcontext - class nullcontext: # type: ignore - """A context manager that does nothing at all. - A fallback for Python 3.7's :class:`contextlib.nullcontext` manager. - """ - - def __init__(self, enter_result=None): - self.enter_result = enter_result - - def __enter__(self): - return self.enter_result - - def __exit__(self, *args): - pass +from .interface import Bus class ThreadSafeBus(ObjectProxy): # pylint: disable=abstract-method diff --git a/can/typechecking.py b/can/typechecking.py index dc5c22270..29c760d31 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -6,15 +6,13 @@ if typing.TYPE_CHECKING: import os -import typing_extensions - -class CanFilter(typing_extensions.TypedDict): +class CanFilter(typing.TypedDict): can_id: int can_mask: int -class CanFilterExtended(typing_extensions.TypedDict): +class CanFilterExtended(typing.TypedDict): can_id: int can_mask: int extended: bool @@ -42,7 +40,7 @@ class CanFilterExtended(typing_extensions.TypedDict): BusConfig = typing.NewType("BusConfig", typing.Dict[str, typing.Any]) -class AutoDetectedConfig(typing_extensions.TypedDict): +class AutoDetectedConfig(typing.TypedDict): interface: str channel: Channel @@ -50,7 +48,7 @@ class AutoDetectedConfig(typing_extensions.TypedDict): ReadableBytesLike = typing.Union[bytes, bytearray, memoryview] -class BitTimingDict(typing_extensions.TypedDict): +class BitTimingDict(typing.TypedDict): f_clock: int brp: int tseg1: int @@ -59,7 +57,7 @@ class BitTimingDict(typing_extensions.TypedDict): nof_samples: int -class BitTimingFdDict(typing_extensions.TypedDict): +class BitTimingFdDict(typing.TypedDict): f_clock: int nom_brp: int nom_tseg1: int diff --git a/pyproject.toml b/pyproject.toml index 65b8f8aed..9ae2de98a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools >= 67.7.2"] +requires = ["setuptools >= 67.7"] build-backend = "setuptools.build_meta" [project] @@ -10,14 +10,13 @@ authors = [{ name = "python-can contributors" }] dependencies = [ "wrapt~=1.10", "packaging >= 23.1", - "setuptools >= 67.7.2", "typing_extensions>=3.10.0.0", "msgpack~=1.0.0; platform_system != 'Windows'", "pywin32>=305; platform_system == 'Windows' and platform_python_implementation == 'CPython'", ] -requires-python = ">=3.7" +requires-python = ">=3.8" license = { text = "LGPL v3" } -classifiers = [ +classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", @@ -32,7 +31,6 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: Python", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -60,12 +58,10 @@ changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] lint = [ - "pylint==2.16.4", - "ruff==0.0.260", - "black~=23.1.0", - "mypy==1.0.1", - "mypy-extensions==0.4.3", - "types-setuptools" + "pylint==2.17.*", + "ruff==0.0.267", + "black==23.3.*", + "mypy==1.3.*", ] seeedstudio = ["pyserial>=3.0"] serial = ["pyserial~=3.0"] @@ -135,7 +131,6 @@ select = [ "F401", # unused-imports "UP", # pyupgrade "I", # isort - ] [tool.ruff.isort] diff --git a/tox.ini b/tox.ini index b48b5642f..477b1d4fc 100644 --- a/tox.ini +++ b/tox.ini @@ -3,8 +3,8 @@ isolated_build = true [testenv] deps = - pytest==7.1.*,>=7.1.2 - pytest-timeout==2.0.2 + pytest==7.3.* + pytest-timeout==2.1.* coveralls==3.3.1 pytest-cov==4.0.0 coverage==6.5.0 From b291f806ec7ce02e3c8256b3102d3a0e7f22f85d Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 20 May 2023 19:23:49 +0200 Subject: [PATCH 11/46] Fix socketcan KeyError (#1599) * use get * adapt test for issue #1598 --- can/interfaces/socketcan/utils.py | 2 +- test/data/ip_link_list.json | 91 +++++++++++++++++++++++++++++++ test/test_socketcan_helpers.py | 15 +---- 3 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 test/data/ip_link_list.json diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py index 8b2114692..91878f9b6 100644 --- a/can/interfaces/socketcan/utils.py +++ b/can/interfaces/socketcan/utils.py @@ -66,7 +66,7 @@ def find_available_interfaces() -> List[str]: output_json, ) - interfaces = [i["ifname"] for i in output_json if i["link_type"] == "can"] + interfaces = [i["ifname"] for i in output_json if i.get("link_type") == "can"] return interfaces diff --git a/test/data/ip_link_list.json b/test/data/ip_link_list.json new file mode 100644 index 000000000..a96313b43 --- /dev/null +++ b/test/data/ip_link_list.json @@ -0,0 +1,91 @@ +[ + { + "ifindex": 1, + "ifname": "lo", + "flags": [ + "LOOPBACK", + "UP", + "LOWER_UP" + ], + "mtu": 65536, + "qdisc": "noqueue", + "operstate": "UNKNOWN", + "linkmode": "DEFAULT", + "group": "default", + "txqlen": 1000, + "link_type": "loopback", + "address": "00:00:00:00:00:00", + "broadcast": "00:00:00:00:00:00" + }, + { + "ifindex": 2, + "ifname": "eth0", + "flags": [ + "NO-CARRIER", + "BROADCAST", + "MULTICAST", + "UP" + ], + "mtu": 1500, + "qdisc": "fq_codel", + "operstate": "DOWN", + "linkmode": "DEFAULT", + "group": "default", + "txqlen": 1000, + "link_type": "ether", + "address": "11:22:33:44:55:66", + "broadcast": "ff:ff:ff:ff:ff:ff" + }, + { + "ifindex": 3, + "ifname": "wlan0", + "flags": [ + "BROADCAST", + "MULTICAST", + "UP", + "LOWER_UP" + ], + "mtu": 1500, + "qdisc": "noqueue", + "operstate": "UP", + "linkmode": "DORMANT", + "group": "default", + "txqlen": 1000, + "link_type": "ether", + "address": "11:22:33:44:55:66", + "broadcast": "ff:ff:ff:ff:ff:ff" + }, + { + "ifindex": 48, + "ifname": "vcan0", + "flags": [ + "NOARP", + "UP", + "LOWER_UP" + ], + "mtu": 72, + "qdisc": "noqueue", + "operstate": "UNKNOWN", + "linkmode": "DEFAULT", + "group": "default", + "txqlen": 1000, + "link_type": "can" + }, + { + "ifindex": 50, + "ifname": "mycustomCan123", + "flags": [ + "NOARP", + "UP", + "LOWER_UP" + ], + "mtu": 72, + "qdisc": "noqueue", + "operstate": "UNKNOWN", + "linkmode": "DEFAULT", + "group": "default", + "txqlen": 1000, + "link_type": "can" + }, + {} +] \ No newline at end of file diff --git a/test/test_socketcan_helpers.py b/test/test_socketcan_helpers.py index 0f4e1b4ea..a1d0bc8af 100644 --- a/test/test_socketcan_helpers.py +++ b/test/test_socketcan_helpers.py @@ -4,9 +4,8 @@ Tests helpers in `can.interfaces.socketcan.socketcan_common`. """ -import gzip import unittest -from base64 import b64decode +from pathlib import Path from unittest import mock from can.interfaces.socketcan.utils import error_code_to_str, find_available_interfaces @@ -42,17 +41,7 @@ def test_find_available_interfaces(self): def test_find_available_interfaces_w_patch(self): # Contains lo, eth0, wlan0, vcan0, mycustomCan123 - ip_output_gz_b64 = ( - "H4sIAAAAAAAAA+2UzW+CMBjG7/wVhrNL+BC29IboEqNSwzQejDEViiMC5aNsmmX/+wpZTGUwDAcP" - "y5qmh+d5++bN80u7EXpsfZRnsUTf8yMXn0TQk/u8GqEQM1EMiMjpXoAOGZM3F6mUZxAuhoY55UpL" - "fbWoKjO4Hts7pl/kLdc+pDlrrmuaqnNq4vqZU8wSkSTHOeYHIjFOM4poOevKmlpwbfF+4EfHkLil" - "PRo/G6vZkrcPKcnjwnOxh/KA8h49JQGOimAkSaq03NFz/B0PiffIOfIXkeumOCtiEiUJXG++bp8S" - "5Dooo/WVZeFnvxmYUgsM01fpBmQWfDAN256M7SqioQ2NkWm8LKvGnIU3qTN+xylrV/FdaHrJzmFk" - "gkacozuzZMnhtAGkLANFAaoKBgOgaUDXG0F6Hrje7SDVWpDvAYpuIdmJV4dn2cSx9VUuGiFCe25Y" - "fwTi4KmW4ptzG0ULGvYPLN1APSqdMN3/82TRtOeqSbW5hmcnzygJTRTJivofcEvAgrAVvgD8aLkv" - "/AcAAA==" - ) - ip_output = gzip.decompress(b64decode(ip_output_gz_b64)).decode("ascii") + ip_output = (Path(__file__).parent / "data" / "ip_link_list.json").read_text() with mock.patch("subprocess.check_output") as check_output: check_output.return_value = ip_output From 88a0dbfa03ae8c9f64aa72ce439a95ac549861d5 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 20 May 2023 20:29:37 +0200 Subject: [PATCH 12/46] activate ruff pycodestyle checks (#1602) --- can/__init__.py | 8 +++--- can/interfaces/ixxat/canlib_vcinpl2.py | 9 +++---- can/interfaces/socketcan/socketcan.py | 23 +++++++++-------- can/interfaces/vector/canlib.py | 34 ++++++++++---------------- can/interfaces/vector/xldriver.py | 9 +------ pyproject.toml | 7 +++++- 6 files changed, 39 insertions(+), 51 deletions(-) diff --git a/can/__init__.py b/can/__init__.py index 34db1727c..034f388a1 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -76,10 +76,6 @@ "viewer", ] -log = logging.getLogger("can") - -rc: Dict[str, Any] = {} - from . import typechecking # isort:skip from . import util # isort:skip from . import broadcastmanager, interface @@ -127,3 +123,7 @@ from .notifier import Notifier from .thread_safe_bus import ThreadSafeBus from .util import set_logging_level + +log = logging.getLogger("can") + +rc: Dict[str, Any] = {} diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index b796be744..2e6e9ad8e 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -13,6 +13,7 @@ import functools import logging import sys +import time import warnings from typing import Callable, Optional, Sequence, Tuple, Union @@ -43,8 +44,6 @@ log = logging.getLogger("can.ixxat") -from time import perf_counter - # Hack to have vciFormatError as a free function, see below vciFormatError = None @@ -144,7 +143,7 @@ def __check_status(result, function, args): _canlib.map_symbol( "vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) ) - except: + except ImportError: _canlib.map_symbol( "vciFormatErrorA", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) ) @@ -812,7 +811,7 @@ def _recv_internal(self, timeout): else: timeout_ms = int(timeout * 1000) remaining_ms = timeout_ms - t0 = perf_counter() + t0 = time.perf_counter() while True: try: @@ -861,7 +860,7 @@ def _recv_internal(self, timeout): log.warning("Unexpected message info type") if t0 is not None: - remaining_ms = timeout_ms - int((perf_counter() - t0) * 1000) + remaining_ms = timeout_ms - int((time.perf_counter() - t0) * 1000) if remaining_ms < 0: break diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 44cecec76..377fe6478 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -17,6 +17,17 @@ import warnings from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type, Union +import can +from can import BusABC, CanProtocol, Message +from can.broadcastmanager import ( + LimitedDurationCyclicSendTaskABC, + ModifiableCyclicTaskABC, + RestartableCyclicTaskABC, +) +from can.interfaces.socketcan import constants +from can.interfaces.socketcan.utils import find_available_interfaces, pack_filters +from can.typechecking import CanFilters + log = logging.getLogger(__name__) log_tx = log.getChild("tx") log_rx = log.getChild("rx") @@ -30,18 +41,6 @@ log.error("socket.CMSG_SPACE not available on this platform") -import can -from can import BusABC, CanProtocol, Message -from can.broadcastmanager import ( - LimitedDurationCyclicSendTaskABC, - ModifiableCyclicTaskABC, - RestartableCyclicTaskABC, -) -from can.interfaces.socketcan import constants -from can.interfaces.socketcan.utils import find_available_interfaces, pack_filters -from can.typechecking import CanFilters - - # Setup BCM struct def bcm_header_factory( fields: List[Tuple[str, Union[Type[ctypes.c_uint32], Type[ctypes.c_long]]]], diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index f852fb0ec..1a84f3d2a 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -4,8 +4,6 @@ Authors: Julien Grave , Christian Sandberg """ -# Import Standard Python Modules -# ============================== import contextlib import ctypes import logging @@ -26,19 +24,6 @@ cast, ) -WaitForSingleObject: Optional[Callable[[int, int], int]] -INFINITE: Optional[int] -try: - # Try builtin Python 3 Windows API - from _winapi import INFINITE, WaitForSingleObject # type: ignore - - HAS_EVENTS = True -except ImportError: - WaitForSingleObject, INFINITE = None, None - HAS_EVENTS = False - -# Import Modules -# ============== from can import ( BitTiming, BitTimingFd, @@ -57,15 +42,11 @@ time_perfcounter_correlation, ) -# Define Module Logger -# ==================== -LOG = logging.getLogger(__name__) - -# Import Vector API modules -# ========================= from . import xlclass, xldefine from .exceptions import VectorError, VectorInitializationError, VectorOperationError +LOG = logging.getLogger(__name__) + # Import safely Vector API module for Travis tests xldriver: Optional[ModuleType] = None try: @@ -73,6 +54,17 @@ except Exception as exc: LOG.warning("Could not import vxlapi: %s", exc) +WaitForSingleObject: Optional[Callable[[int, int], int]] +INFINITE: Optional[int] +try: + # Try builtin Python 3 Windows API + from _winapi import INFINITE, WaitForSingleObject # type: ignore + + HAS_EVENTS = True +except ImportError: + WaitForSingleObject, INFINITE = None, None + HAS_EVENTS = False + class VectorBus(BusABC): """The CAN Bus implemented for the Vector interface.""" diff --git a/can/interfaces/vector/xldriver.py b/can/interfaces/vector/xldriver.py index f84b3cf1d..2af90c728 100644 --- a/can/interfaces/vector/xldriver.py +++ b/can/interfaces/vector/xldriver.py @@ -5,22 +5,15 @@ Authors: Julien Grave , Christian Sandberg """ -# Import Standard Python Modules -# ============================== import ctypes import logging import platform +from . import xlclass from .exceptions import VectorInitializationError, VectorOperationError -# Define Module Logger -# ==================== LOG = logging.getLogger(__name__) -# Vector XL API Definitions -# ========================= -from . import xlclass - # Load Windows DLL DLL_NAME = "vxlapi64" if platform.architecture()[0] == "64bit" else "vxlapi" _xlapi_dll = ctypes.windll.LoadLibrary(DLL_NAME) diff --git a/pyproject.toml b/pyproject.toml index 9ae2de98a..8427c5655 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] lint = [ "pylint==2.17.*", - "ruff==0.0.267", + "ruff==0.0.269", "black==23.3.*", "mypy==1.3.*", ] @@ -131,6 +131,11 @@ select = [ "F401", # unused-imports "UP", # pyupgrade "I", # isort + "E", # pycodestyle errors + "W", # pycodestyle warnings +] +ignore = [ + "E501", # Line too long ] [tool.ruff.isort] From e8aa4e790ed7029a1706134a58242c16662ad761 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 20 May 2023 20:31:24 +0200 Subject: [PATCH 13/46] Update linter instructions in development.rst (#1603) --- doc/development.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/development.rst b/doc/development.rst index cfb8dbe5d..fb717a52d 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -53,9 +53,11 @@ The documentation can be built with:: The linters can be run with:: - pip install -r requirements-lint.txt - pylint --rcfile=.pylintrc-wip can/**.py - black --check --verbose can + pip install -e .[lint] + black --check can + mypy can + ruff check can + pylint --rcfile=.pylintrc can/**.py Creating a new interface/backend From 2582fe975c189471b0c439c1488909d48b219bdf Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 21 May 2023 14:01:08 +0200 Subject: [PATCH 14/46] remove unnecessary script files (#1604) --- doc/scripts.rst | 2 +- pyproject.toml | 8 ++++---- scripts/can_logconvert.py | 11 ----------- scripts/can_logger.py | 11 ----------- scripts/can_player.py | 11 ----------- scripts/can_viewer.py | 11 ----------- test/test_scripts.py | 12 +++--------- 7 files changed, 8 insertions(+), 58 deletions(-) delete mode 100644 scripts/can_logconvert.py delete mode 100644 scripts/can_logger.py delete mode 100644 scripts/can_player.py delete mode 100644 scripts/can_viewer.py diff --git a/doc/scripts.rst b/doc/scripts.rst index 5a615afa7..e3a59a409 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -3,7 +3,7 @@ Scripts The following modules are callable from ``python-can``. -They can be called for example by ``python -m can.logger`` or ``can_logger.py`` (if installed using pip). +They can be called for example by ``python -m can.logger`` or ``can_logger`` (if installed using pip). can.logger ---------- diff --git a/pyproject.toml b/pyproject.toml index 8427c5655..6ee1dbadc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,10 +45,10 @@ classifiers = [ ] [project.scripts] -"can_logconvert.py" = "scripts.can_logconvert:main" -"can_logger.py" = "scripts.can_logger:main" -"can_player.py" = "scripts.can_player:main" -"can_viewer.py" = "scripts.can_viewer:main" +can_logconvert = "can.logconvert:main" +can_logger = "can.logger:main" +can_player = "can.player:main" +can_viewer = "can.viewer:main" [project.urls] homepage = "https://github.com/hardbyte/python-can" diff --git a/scripts/can_logconvert.py b/scripts/can_logconvert.py deleted file mode 100644 index 3cd8839a2..000000000 --- a/scripts/can_logconvert.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python - -""" -See :mod:`can.logconvert`. -""" - -from can.logconvert import main - - -if __name__ == "__main__": - main() diff --git a/scripts/can_logger.py b/scripts/can_logger.py deleted file mode 100644 index 4202448e6..000000000 --- a/scripts/can_logger.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python - -""" -See :mod:`can.logger`. -""" - -from can.logger import main - - -if __name__ == "__main__": - main() diff --git a/scripts/can_player.py b/scripts/can_player.py deleted file mode 100644 index 1fe44175d..000000000 --- a/scripts/can_player.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python - -""" -See :mod:`can.player`. -""" - -from can.player import main - - -if __name__ == "__main__": - main() diff --git a/scripts/can_viewer.py b/scripts/can_viewer.py deleted file mode 100644 index eef990b0e..000000000 --- a/scripts/can_viewer.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python - -""" -See :mod:`can.viewer`. -""" - -from can.viewer import main - - -if __name__ == "__main__": - main() diff --git a/test/test_scripts.py b/test/test_scripts.py index e7bd7fd09..9d8c059cf 100644 --- a/test/test_scripts.py +++ b/test/test_scripts.py @@ -74,10 +74,8 @@ class TestLoggerScript(CanScriptTest): def _commands(self): commands = [ "python -m can.logger --help", - "python scripts/can_logger.py --help", + "can_logger --help", ] - if IS_UNIX: - commands += ["can_logger.py --help"] return commands def _import(self): @@ -90,10 +88,8 @@ class TestPlayerScript(CanScriptTest): def _commands(self): commands = [ "python -m can.player --help", - "python scripts/can_player.py --help", + "can_player --help", ] - if IS_UNIX: - commands += ["can_player.py --help"] return commands def _import(self): @@ -106,10 +102,8 @@ class TestLogconvertScript(CanScriptTest): def _commands(self): commands = [ "python -m can.logconvert --help", - "python scripts/can_logconvert.py --help", + "can_logconvert --help", ] - if IS_UNIX: - commands += ["can_logconvert.py --help"] return commands def _import(self): From 94125139002af2f1a11d13186ab653821c954ebd Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Thu, 1 Jun 2023 00:07:10 +0200 Subject: [PATCH 15/46] fix IXXAT not properly shut down message (#1606) --- can/interfaces/ixxat/canlib_vcinpl.py | 1 + can/interfaces/ixxat/canlib_vcinpl2.py | 1 + 2 files changed, 2 insertions(+) diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py index cbf2fb61c..1bb0fd802 100644 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ b/can/interfaces/ixxat/canlib_vcinpl.py @@ -825,6 +825,7 @@ def _send_periodic_internal( ) def shutdown(self): + super().shutdown() if self._scheduler is not None: _canlib.canSchedulerClose(self._scheduler) _canlib.canChannelClose(self._channel_handle) diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index 2e6e9ad8e..b10ac1b94 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -972,6 +972,7 @@ def _send_periodic_internal( ) def shutdown(self): + super().shutdown() if self._scheduler is not None: _canlib.canSchedulerClose(self._scheduler) _canlib.canChannelClose(self._channel_handle) From 1df66043926dfe455b09941cf75dfeb13b2d2bbc Mon Sep 17 00:00:00 2001 From: MattWoodhead Date: Fri, 2 Jun 2023 17:34:26 +0100 Subject: [PATCH 16/46] Can Player compatibility with interfaces that use additional configuration (#1610) * Update mf4.py * Format code with black * Update trc.py * Format code with black * Update ci.yml Remove pylint specs for directories and files that no longer exist. --------- Co-authored-by: MattWoodhead --- .github/workflows/ci.yml | 2 -- can/io/mf4.py | 6 +++++- can/io/trc.py | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9633398e3..686a5d77b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,9 +103,7 @@ jobs: pylint --rcfile=.pylintrc \ can/**.py \ can/io \ - setup.py \ doc/conf.py \ - scripts/**.py \ examples/**.py \ can/interfaces/socketcan diff --git a/can/io/mf4.py b/can/io/mf4.py index 215543e9f..c7e71e816 100644 --- a/can/io/mf4.py +++ b/can/io/mf4.py @@ -272,7 +272,11 @@ class MF4Reader(BinaryIOMessageReader): The MF4Reader only supports MF4 files that were recorded with python-can. """ - def __init__(self, file: Union[StringPathLike, BinaryIO]) -> None: + def __init__( + self, + file: Union[StringPathLike, BinaryIO], + **kwargs: Any, + ) -> None: """ :param file: a path-like object or as file-like object to read from If this is a file-like object, is has to be opened in diff --git a/can/io/trc.py b/can/io/trc.py index f116bdc04..ccf122d57 100644 --- a/can/io/trc.py +++ b/can/io/trc.py @@ -11,7 +11,7 @@ import os from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Callable, Dict, Generator, List, Optional, TextIO, Union +from typing import Any, Callable, Dict, Generator, List, Optional, TextIO, Union from ..message import Message from ..typechecking import StringPathLike @@ -49,6 +49,7 @@ class TRCReader(TextIOMessageReader): def __init__( self, file: Union[StringPathLike, TextIO], + **kwargs: Any, ) -> None: """ :param file: a path-like object or as file-like object to read from @@ -265,6 +266,7 @@ def __init__( self, file: Union[StringPathLike, TextIO], channel: int = 1, + **kwargs: Any, ) -> None: """ :param file: a path-like object or as file-like object to write to From 67324f158318e613ba3226704a9985124dd3caff Mon Sep 17 00:00:00 2001 From: Lukas Magel Date: Tue, 13 Jun 2023 22:15:19 +0200 Subject: [PATCH 17/46] Fix decoding error in Kvaser constructor for non-ASCII product name (#1613) * Implement test to trigger ASCII decoding error in Kvaser bus constructor * Change handling of invalid chars in Kvaser device name Previously, illegal characters triggered an exception. With the new behavior, illegal characters will be replaced with a placeholder value. * Rename variables in test --- can/interfaces/kvaser/canlib.py | 3 ++- test/test_kvaser.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 32d28059a..4e5e8c51b 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -727,7 +727,8 @@ def get_channel_info(channel): ctypes.sizeof(number), ) - return f"{name.value.decode('ascii')}, S/N {serial.value} (#{number.value + 1})" + name_decoded = name.value.decode("ascii", errors="replace") + return f"{name_decoded}, S/N {serial.value} (#{number.value + 1})" init_kvaser_library() diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 043f86f8c..1254f2fc7 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -3,6 +3,7 @@ """ """ +import ctypes import time import unittest from unittest.mock import Mock @@ -49,6 +50,26 @@ def test_bus_creation(self): self.assertTrue(canlib.canOpenChannel.called) self.assertTrue(canlib.canBusOn.called) + def test_bus_creation_illegal_channel_name(self): + # Test if the bus constructor is able to deal with non-ASCII characters + def canGetChannelDataMock( + channel: ctypes.c_int, + param: ctypes.c_int, + buf: ctypes.c_void_p, + bufsize: ctypes.c_size_t, + ): + if param == constants.canCHANNELDATA_DEVDESCR_ASCII: + buf_char_ptr = ctypes.cast(buf, ctypes.POINTER(ctypes.c_char)) + for i, char in enumerate(b"hello\x7a\xcb"): + buf_char_ptr[i] = char + + canlib.canGetChannelData = canGetChannelDataMock + bus = can.Bus(channel=0, interface="kvaser") + + self.assertTrue(bus.channel_info.startswith("hello")) + + bus.shutdown() + def test_bus_shutdown(self): self.bus.shutdown() self.assertTrue(canlib.canBusOff.called) From dc0ae68acb28f01a503e084718cfa2acf39d1e16 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 18 Jun 2023 17:23:56 +0200 Subject: [PATCH 18/46] update CHANGELOG.md and version to 4.2.2 (#1615) --- CHANGELOG.md | 11 +++++++++++ can/__init__.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a240cb650..493ac1966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +Version 4.2.2 +============= + +Bug Fixes +--------- +* Fix socketcan KeyError (#1598, #1599). +* Fix IXXAT not properly shutdown message (#1606). +* Fix Mf4Reader and TRCReader incompatibility with extra CLI args (#1610). +* Fix decoding error in Kvaser constructor for non-ASCII product name (#1613). + + Version 4.2.1 ============= diff --git a/can/__init__.py b/can/__init__.py index 034f388a1..46a461b38 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging from typing import Any, Dict -__version__ = "4.2.1" +__version__ = "4.2.2" __all__ = [ "ASCReader", "ASCWriter", From c5d7a7ce71e5e12cad4682d6c1411f8688de54bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?uml=C3=A4ute?= Date: Fri, 30 Jun 2023 11:18:46 +0200 Subject: [PATCH 19/46] BigEndian test fixes (#1625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PCAN_BITRATES are stored in BIGendian, no use to byteswap on littleendian. Also the TPCANChannelInformation expects data in the native byte order Closes: https://github.com/hardbyte/python-can/issues/1624 Co-authored-by: IOhannes m zmölnig (Debian/GNU) --- test/test_bit_timing.py | 2 +- test/test_pcan.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/test/test_bit_timing.py b/test/test_bit_timing.py index a0d4e03a5..d0d0a4a7f 100644 --- a/test/test_bit_timing.py +++ b/test/test_bit_timing.py @@ -175,7 +175,7 @@ def test_from_btr(): def test_btr_persistence(): f_clock = 8_000_000 for btr0btr1 in PCAN_BITRATES.values(): - btr1, btr0 = struct.unpack("BB", btr0btr1) + btr0, btr1 = struct.pack(">H", btr0btr1.value) t = can.BitTiming.from_registers(f_clock, btr0, btr1) assert t.btr0 == btr0 diff --git a/test/test_pcan.py b/test/test_pcan.py index 19fa44dc7..be6c5ad64 100644 --- a/test/test_pcan.py +++ b/test/test_pcan.py @@ -3,6 +3,7 @@ """ import ctypes +import struct import unittest from unittest import mock from unittest.mock import Mock, patch @@ -379,9 +380,7 @@ def test_detect_available_configs(self) -> None: self.assertEqual(len(configs), 50) else: value = (TPCANChannelInformation * 1).from_buffer_copy( - b"Q\x00\x05\x00\x01\x00\x00\x00PCAN-USB FD\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b'\x00\x00\x00\x00\x00\x00\x003"\x11\x00\x01\x00\x00\x00' + struct.pack("HBBI33sII", 81, 5, 0, 1, b"PCAN-USB FD", 1122867, 1) ) self.mock_pcan.GetValue = Mock(return_value=(PCAN_ERROR_OK, value)) configs = PcanBus._detect_available_configs() From 78d25ff6c9a63a084f8add6976d4bf1817a553be Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Thu, 6 Jul 2023 10:13:34 -0400 Subject: [PATCH 20/46] Enable send and receive on network ID above 255 (#1627) --- can/interfaces/ics_neovi/neovi_bus.py | 55 ++++++++++++--------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index f2dffe0a6..f78293c86 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -12,6 +12,7 @@ import os import tempfile from collections import Counter, defaultdict, deque +from functools import partial from itertools import cycle from threading import Event from warnings import warn @@ -317,7 +318,8 @@ def _process_msg_queue(self, timeout=0.1): except ics.RuntimeError: return for ics_msg in messages: - if ics_msg.NetworkID not in self.channels: + channel = ics_msg.NetworkID | (ics_msg.NetworkID2 << 8) + if channel not in self.channels: continue is_tx = bool(ics_msg.StatusBitField & ics.SPY_STATUS_TX_MSG) @@ -364,50 +366,37 @@ def _get_timestamp_for_msg(self, ics_msg): def _ics_msg_to_message(self, ics_msg): is_fd = ics_msg.Protocol == ics.SPY_PROTOCOL_CANFD + message_from_ics = partial( + Message, + timestamp=self._get_timestamp_for_msg(ics_msg), + arbitration_id=ics_msg.ArbIDOrHeader, + is_extended_id=bool(ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME), + is_remote_frame=bool(ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME), + is_error_frame=bool(ics_msg.StatusBitField2 & ics.SPY_STATUS2_ERROR_FRAME), + channel=ics_msg.NetworkID | (ics_msg.NetworkID2 << 8), + dlc=ics_msg.NumberBytesData, + is_fd=is_fd, + is_rx=not bool(ics_msg.StatusBitField & ics.SPY_STATUS_TX_MSG), + ) + if is_fd: if ics_msg.ExtraDataPtrEnabled: data = ics_msg.ExtraDataPtr[: ics_msg.NumberBytesData] else: data = ics_msg.Data[: ics_msg.NumberBytesData] - return Message( - timestamp=self._get_timestamp_for_msg(ics_msg), - arbitration_id=ics_msg.ArbIDOrHeader, + return message_from_ics( data=data, - dlc=ics_msg.NumberBytesData, - is_extended_id=bool(ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME), - is_fd=is_fd, - is_rx=not bool(ics_msg.StatusBitField & ics.SPY_STATUS_TX_MSG), - is_remote_frame=bool( - ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME - ), - is_error_frame=bool( - ics_msg.StatusBitField2 & ics.SPY_STATUS2_ERROR_FRAME - ), error_state_indicator=bool( ics_msg.StatusBitField3 & ics.SPY_STATUS3_CANFD_ESI ), bitrate_switch=bool( ics_msg.StatusBitField3 & ics.SPY_STATUS3_CANFD_BRS ), - channel=ics_msg.NetworkID, ) else: - return Message( - timestamp=self._get_timestamp_for_msg(ics_msg), - arbitration_id=ics_msg.ArbIDOrHeader, + return message_from_ics( data=ics_msg.Data[: ics_msg.NumberBytesData], - dlc=ics_msg.NumberBytesData, - is_extended_id=bool(ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME), - is_fd=is_fd, - is_rx=not bool(ics_msg.StatusBitField & ics.SPY_STATUS_TX_MSG), - is_remote_frame=bool( - ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME - ), - is_error_frame=bool( - ics_msg.StatusBitField2 & ics.SPY_STATUS2_ERROR_FRAME - ), - channel=ics_msg.NetworkID, ) def _recv_internal(self, timeout=0.1): @@ -479,12 +468,16 @@ def send(self, msg, timeout=0): message.StatusBitField2 = 0 message.StatusBitField3 = flag3 if msg.channel is not None: - message.NetworkID = msg.channel + network_id = msg.channel elif len(self.channels) == 1: - message.NetworkID = self.channels[0] + network_id = self.channels[0] else: raise ValueError("msg.channel must be set when using multiple channels.") + message.NetworkID, message.NetworkID2 = int(network_id & 0xFF), int( + (network_id >> 8) & 0xFF + ) + if timeout != 0: msg_desc_id = next(description_id) message.DescriptionID = msg_desc_id From ddc7c35d9f6cf8bac8da5531fac44dee56ccc90b Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 9 Jul 2023 14:00:41 +0200 Subject: [PATCH 21/46] Fix Vector channel detection (#1634) --- can/interfaces/vector/canlib.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 1a84f3d2a..55aeb5da4 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -400,7 +400,11 @@ def _read_bus_params(self, channel: int) -> "VectorBusParams": vcc_list = get_channel_configs() for vcc in vcc_list: if vcc.channel_mask == channel_mask: - return vcc.bus_params + bus_params = vcc.bus_params + if bus_params is None: + # for CAN channels, this should never be `None` + raise ValueError("Invalid bus parameters.") + return bus_params raise CanInitializationError( f"Channel configuration for channel {channel} not found." @@ -1090,7 +1094,7 @@ class VectorChannelConfig(NamedTuple): channel_bus_capabilities: xldefine.XL_BusCapabilities is_on_bus: bool connected_bus_type: xldefine.XL_BusTypes - bus_params: VectorBusParams + bus_params: Optional[VectorBusParams] serial_number: int article_number: int transceiver_name: str @@ -1110,9 +1114,14 @@ def _get_xl_driver_config() -> xlclass.XLdriverConfig: return driver_config -def _read_bus_params_from_c_struct(bus_params: xlclass.XLbusParams) -> VectorBusParams: +def _read_bus_params_from_c_struct( + bus_params: xlclass.XLbusParams, +) -> Optional[VectorBusParams]: + bus_type = xldefine.XL_BusTypes(bus_params.busType) + if bus_type is not xldefine.XL_BusTypes.XL_BUS_TYPE_CAN: + return None return VectorBusParams( - bus_type=xldefine.XL_BusTypes(bus_params.busType), + bus_type=bus_type, can=VectorCanParams( bitrate=bus_params.data.can.bitRate, sjw=bus_params.data.can.sjw, From 631f7bea71134e1bc8c1af9d6a87f1b75985ff7c Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Tue, 11 Jul 2023 20:35:40 +0200 Subject: [PATCH 22/46] align `ID:` in message string (#1635) --- can/message.py | 6 +++--- doc/message.rst | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/can/message.py b/can/message.py index 02706583c..63a4eea41 100644 --- a/can/message.py +++ b/can/message.py @@ -110,10 +110,10 @@ def __init__( # pylint: disable=too-many-locals, too-many-arguments def __str__(self) -> str: field_strings = [f"Timestamp: {self.timestamp:>15.6f}"] if self.is_extended_id: - arbitration_id_string = f"ID: {self.arbitration_id:08x}" + arbitration_id_string = f"{self.arbitration_id:08x}" else: - arbitration_id_string = f"ID: {self.arbitration_id:04x}" - field_strings.append(arbitration_id_string.rjust(12, " ")) + arbitration_id_string = f"{self.arbitration_id:03x}" + field_strings.append(f"ID: {arbitration_id_string:>8}") flag_string = " ".join( [ diff --git a/doc/message.rst b/doc/message.rst index d47473e17..e0003cfe5 100644 --- a/doc/message.rst +++ b/doc/message.rst @@ -44,7 +44,7 @@ Message 2\ :sup:`29` - 1 for 29-bit identifiers). >>> print(Message(is_extended_id=False, arbitration_id=100)) - Timestamp: 0.000000 ID: 0064 S Rx DL: 0 + Timestamp: 0.000000 ID: 064 S Rx DL: 0 .. attribute:: data @@ -106,7 +106,7 @@ Message Previously this was exposed as `id_type`. >>> print(Message(is_extended_id=False)) - Timestamp: 0.000000 ID: 0000 S Rx DL: 0 + Timestamp: 0.000000 ID: 000 S Rx DL: 0 >>> print(Message(is_extended_id=True)) Timestamp: 0.000000 ID: 00000000 X Rx DL: 0 From d81a16b2a85fec16611ed0bdfdcba8ac54bdc02a Mon Sep 17 00:00:00 2001 From: Yann poupon <100286656+yannpoupon@users.noreply.github.com> Date: Thu, 10 Aug 2023 11:32:26 +0200 Subject: [PATCH 23/46] Optimize PCAN send performance (#1640) --- can/interfaces/pcan/pcan.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index a9b2c016b..13775e983 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -608,8 +608,7 @@ def send(self, msg, timeout=None): CANMsg.MSGTYPE = msgType # copy data - for i in range(msg.dlc): - CANMsg.DATA[i] = msg.data[i] + CANMsg.DATA[: msg.dlc] = msg.data[: msg.dlc] log.debug("Data: %s", msg.data) log.debug("Type: %s", type(msg.data)) @@ -628,8 +627,7 @@ def send(self, msg, timeout=None): # if a remote frame will be sent, data bytes are not important. if not msg.is_remote_frame: # copy data - for i in range(CANMsg.LEN): - CANMsg.DATA[i] = msg.data[i] + CANMsg.DATA[: CANMsg.LEN] = msg.data[: CANMsg.LEN] log.debug("Data: %s", msg.data) log.debug("Type: %s", type(msg.data)) From 38142543fb87441532e71c956179e7bdddba8e59 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Thu, 17 Aug 2023 09:35:01 +0200 Subject: [PATCH 24/46] Support version string of older PCAN basic API (#1644) --- can/interfaces/pcan/pcan.py | 5 ++++- test/test_pcan.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 13775e983..d2973ade6 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -416,7 +416,10 @@ def get_api_version(self): if error != PCAN_ERROR_OK: raise CanInitializationError(f"Failed to read pcan basic api version") - return version.parse(value.decode("ascii")) + # fix https://github.com/hardbyte/python-can/issues/1642 + version_string = value.decode("ascii").replace(",", ".").replace(" ", "") + + return version.parse(version_string) def check_api_version(self): apv = self.get_api_version() diff --git a/test/test_pcan.py b/test/test_pcan.py index be6c5ad64..9f4e36fc4 100644 --- a/test/test_pcan.py +++ b/test/test_pcan.py @@ -120,6 +120,19 @@ def test_api_version_read_fail(self) -> None: with self.assertRaises(CanInitializationError): self.bus = can.Bus(interface="pcan") + def test_issue1642(self) -> None: + self.PCAN_API_VERSION_SIM = "1, 3, 0, 50" + with self.assertLogs("can.pcan", level="WARNING") as cm: + self.bus = can.Bus(interface="pcan") + found_version_warning = False + for i in cm.output: + if "version" in i and "pcan" in i: + found_version_warning = True + self.assertTrue( + found_version_warning, + f"No warning was logged for incompatible api version {cm.output}", + ) + @parameterized.expand( [ ("no_error", PCAN_ERROR_OK, PCAN_ERROR_OK, "some ok text 1"), From fcf615d4023494f455854a22cdfb8072c35be033 Mon Sep 17 00:00:00 2001 From: Fabian Henze <32638720+henzef@users.noreply.github.com> Date: Fri, 18 Aug 2023 18:38:47 +0200 Subject: [PATCH 25/46] ixxat: Fix exception in 'state' property on bus coupling errors (#1647) BusState is not an exception type and should be raised (especially not in a property). --- can/interfaces/ixxat/canlib_vcinpl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py index 1bb0fd802..922c683b8 100644 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ b/can/interfaces/ixxat/canlib_vcinpl.py @@ -852,7 +852,7 @@ def state(self) -> BusState: error_byte_2 = status.dwStatus & 0xF0 # CAN_STATUS_BUSCERR = 0x20 # bus coupling error if error_byte_2 & constants.CAN_STATUS_BUSCERR: - raise BusState.ERROR + return BusState.ERROR return BusState.ACTIVE From 436a7c3da411c54ee407a6e64477b88e1b6e985f Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Wed, 23 Aug 2023 18:13:16 +0200 Subject: [PATCH 26/46] fix pepy badge url (#1649) --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 07d2b0668..d2f05b2c1 100644 --- a/README.rst +++ b/README.rst @@ -17,11 +17,11 @@ python-can :target: https://pypi.python.org/pypi/python-can/ :alt: Supported Python implementations -.. |downloads| image:: https://pepy.tech/badge/python-can +.. |downloads| image:: https://static.pepy.tech/badge/python-can :target: https://pepy.tech/project/python-can :alt: Downloads on PePy -.. |downloads_monthly| image:: https://pepy.tech/badge/python-can/month +.. |downloads_monthly| image:: https://static.pepy.tech/badge/python-can/month :target: https://pepy.tech/project/python-can :alt: Monthly downloads on PePy From 4267e44e336f66641f9be27c4353cd4df9298961 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Wed, 23 Aug 2023 18:14:14 +0200 Subject: [PATCH 27/46] Do not stop notifier if exception was handled (#1645) --- can/notifier.py | 48 +++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/can/notifier.py b/can/notifier.py index 92a91f8a9..1c8b77c5d 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -3,10 +3,11 @@ """ import asyncio +import functools import logging import threading import time -from typing import Awaitable, Callable, Iterable, List, Optional, Union +from typing import Awaitable, Callable, Iterable, List, Optional, Union, cast from can.bus import BusABC from can.listener import Listener @@ -108,28 +109,33 @@ def stop(self, timeout: float = 5) -> None: listener.stop() def _rx_thread(self, bus: BusABC) -> None: - try: - while self._running: + # determine message handling callable early, not inside while loop + handle_message = cast( + Callable[[Message], None], + self._on_message_received + if self._loop is None + else functools.partial( + self._loop.call_soon_threadsafe, self._on_message_received + ), + ) + + while self._running: + try: if msg := bus.recv(self.timeout): with self._lock: - if self._loop is not None: - self._loop.call_soon_threadsafe( - self._on_message_received, msg - ) - else: - self._on_message_received(msg) - except Exception as exc: # pylint: disable=broad-except - self.exception = exc - if self._loop is not None: - self._loop.call_soon_threadsafe(self._on_error, exc) - # Raise anyway - raise - elif not self._on_error(exc): - # If it was not handled, raise the exception here - raise - else: - # It was handled, so only log it - logger.info("suppressed exception: %s", exc) + handle_message(msg) + except Exception as exc: # pylint: disable=broad-except + self.exception = exc + if self._loop is not None: + self._loop.call_soon_threadsafe(self._on_error, exc) + # Raise anyway + raise + elif not self._on_error(exc): + # If it was not handled, raise the exception here + raise + else: + # It was handled, so only log it + logger.debug("suppressed exception: %s", exc) def _on_message_available(self, bus: BusABC) -> None: if msg := bus.recv(0): From 1774051c3298be2ecb7e374ebb1f088365430c0b Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Fri, 25 Aug 2023 10:21:45 -0400 Subject: [PATCH 28/46] Fixed serial number range (#1650) Fix lower serial number value --- can/interfaces/ics_neovi/neovi_bus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index f78293c86..0698c1416 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -255,7 +255,7 @@ def get_serial_number(device): :return: ics device serial string :rtype: str """ - if int("AA0000", 36) < device.SerialNumber < int("ZZZZZZ", 36): + if int("0A0000", 36) < device.SerialNumber < int("ZZZZZZ", 36): return ics.base36enc(device.SerialNumber) else: return str(device.SerialNumber) From 4b17b9c0c24e302627e41ea5dce17b86234016b9 Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Fri, 8 Sep 2023 09:04:40 -0400 Subject: [PATCH 29/46] Use same configuration file as Linux on macOS (#1657) --- can/util.py | 2 +- doc/configuration.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/can/util.py b/can/util.py index 59abdd579..71980e82f 100644 --- a/can/util.py +++ b/can/util.py @@ -43,7 +43,7 @@ CONFIG_FILES = ["~/can.conf"] -if platform.system() == "Linux": +if platform.system() in ("Linux", "Darwin"): CONFIG_FILES.extend(["/etc/can.conf", "~/.can", "~/.canrc"]) elif platform.system() == "Windows" or platform.python_implementation() == "IronPython": CONFIG_FILES.extend(["can.ini", os.path.join(os.getenv("APPDATA", ""), "can.ini")]) diff --git a/doc/configuration.rst b/doc/configuration.rst index 494351350..7b42017a9 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -36,7 +36,7 @@ You can also specify the interface and channel for each Bus instance:: Configuration File ------------------ -On Linux systems the config file is searched in the following paths: +On Linux and macOS systems the config file is searched in the following paths: #. ``~/can.conf`` #. ``/etc/can.conf`` @@ -159,4 +159,4 @@ Lookup table of interface names: | ``"virtual"`` | :doc:`interfaces/virtual` | +---------------------+-------------------------------------+ -Additional interface types can be added via the :ref:`plugin interface`. \ No newline at end of file +Additional interface types can be added via the :ref:`plugin interface`. From 40c779130baac24b3c94ae98696d7fdf99f6a142 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 8 Sep 2023 23:16:30 +0200 Subject: [PATCH 30/46] Fix PCAN timestamp (#1651) * fix PCAN timestamp * remove unused datetime import --- can/interfaces/pcan/pcan.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index d2973ade6..25610a614 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -5,7 +5,6 @@ import platform import time import warnings -from datetime import datetime from typing import Any, List, Optional, Tuple, Union from packaging import version @@ -85,7 +84,7 @@ if uptime.boottime() is None: boottimeEpoch = 0 else: - boottimeEpoch = (uptime.boottime() - datetime.fromtimestamp(0)).total_seconds() + boottimeEpoch = uptime.boottime().timestamp() except ImportError as error: log.warning( "uptime library not available, timestamps are relative to boot time and not to Epoch UTC", From f3fa07107d08c6225b9ab77443df13507c1a9c4b Mon Sep 17 00:00:00 2001 From: luojiaaoo <62821977+luojiaaoo@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:11:04 +0800 Subject: [PATCH 31/46] Kvaser: add parameter exclusive and override_exclusive (#1660) * * For interface Kvaser, add parameter exclusive Don't allow sharing of this CANlib channel. * For interface Kvaser, add parameter override_exclusive Open the channel even if it is opened for exclusive access already. * fix --------- Co-authored-by: luoja --- can/interfaces/kvaser/canlib.py | 17 ++++++++++++++++- can/interfaces/kvaser/constants.py | 2 ++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 4e5e8c51b..38949137d 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -404,6 +404,10 @@ def __init__(self, channel, can_filters=None, **kwargs): computer, set this to True or set single_handle to True. :param bool fd: If CAN-FD frames should be supported. + :param bool exclusive: + Don't allow sharing of this CANlib channel. + :param bool override_exclusive: + Open the channel even if it is opened for exclusive access already. :param int data_bitrate: Which bitrate to use for data phase in CAN FD. Defaults to arbitration bitrate. @@ -420,6 +424,8 @@ def __init__(self, channel, can_filters=None, **kwargs): driver_mode = kwargs.get("driver_mode", DRIVER_MODE_NORMAL) single_handle = kwargs.get("single_handle", False) receive_own_messages = kwargs.get("receive_own_messages", False) + exclusive = kwargs.get("exclusive", False) + override_exclusive = kwargs.get("override_exclusive", False) accept_virtual = kwargs.get("accept_virtual", True) fd = kwargs.get("fd", False) data_bitrate = kwargs.get("data_bitrate", None) @@ -445,6 +451,10 @@ def __init__(self, channel, can_filters=None, **kwargs): self.channel_info = channel_info flags = 0 + if exclusive: + flags |= canstat.canOPEN_EXCLUSIVE + if override_exclusive: + flags |= canstat.canOPEN_OVERRIDE_EXCLUSIVE if accept_virtual: flags |= canstat.canOPEN_ACCEPT_VIRTUAL if fd: @@ -491,7 +501,12 @@ def __init__(self, channel, can_filters=None, **kwargs): self._write_handle = self._read_handle else: log.debug("Creating separate handle for TX on channel: %s", channel) - self._write_handle = canOpenChannel(channel, flags) + if exclusive: + flags_ = flags & ~canstat.canOPEN_EXCLUSIVE + flags_ |= canstat.canOPEN_OVERRIDE_EXCLUSIVE + else: + flags_ = flags + self._write_handle = canOpenChannel(channel, flags_) canBusOn(self._read_handle) can_driver_mode = ( diff --git a/can/interfaces/kvaser/constants.py b/can/interfaces/kvaser/constants.py index 9dd3a9163..3d01faa84 100644 --- a/can/interfaces/kvaser/constants.py +++ b/can/interfaces/kvaser/constants.py @@ -161,6 +161,8 @@ def CANSTATUS_SUCCESS(status): canDRIVER_SELFRECEPTION = 8 canDRIVER_OFF = 0 +canOPEN_EXCLUSIVE = 0x0008 +canOPEN_REQUIRE_EXTENDED = 0x0010 canOPEN_ACCEPT_VIRTUAL = 0x0020 canOPEN_OVERRIDE_EXCLUSIVE = 0x0040 canOPEN_REQUIRE_INIT_ACCESS = 0x0080 From b794153a5744afecb4113f3f814a5c94695e9f76 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 17 Sep 2023 22:04:43 +0200 Subject: [PATCH 32/46] Catch `pywintypes.error` in broadcast manager (#1659) --- can/broadcastmanager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 84554a507..a017b7d52 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -22,6 +22,7 @@ # try to import win32event for event-based cyclic send task (needs the pywin32 package) USE_WINDOWS_EVENTS = False try: + import pywintypes import win32event # Python 3.11 provides a more precise sleep implementation on Windows, so this is not necessary. @@ -263,7 +264,7 @@ def __init__( win32event.CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, win32event.TIMER_ALL_ACCESS, ) - except (AttributeError, OSError): + except (AttributeError, OSError, pywintypes.error): self.event = win32event.CreateWaitableTimer(None, False, None) self.start() From e09d35e568cdd32cd3137e6813482988c6925a4e Mon Sep 17 00:00:00 2001 From: ro-id <84511204+ro-id@users.noreply.github.com> Date: Sun, 17 Sep 2023 22:06:17 +0200 Subject: [PATCH 33/46] Fix BLFReader error for incomplete or truncated stream (#1662) * bugfix BLFreader zlib.error: Error -5 while decompressing data: incomplete or truncated stream in Python * delete elif copied on to many lines * Update blf.py shorten line -> comment in next line delete unused variable * fromatting --------- Co-authored-by: Iding Robin --- can/io/blf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/can/io/blf.py b/can/io/blf.py index 071c089d7..81146233d 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -182,12 +182,13 @@ def __iter__(self) -> Generator[Message, None, None]: self.file.read(obj_size % 4) if obj_type == LOG_CONTAINER: - method, uncompressed_size = LOG_CONTAINER_STRUCT.unpack_from(obj_data) + method, _ = LOG_CONTAINER_STRUCT.unpack_from(obj_data) container_data = obj_data[LOG_CONTAINER_STRUCT.size :] if method == NO_COMPRESSION: data = container_data elif method == ZLIB_DEFLATE: - data = zlib.decompress(container_data, 15, uncompressed_size) + zobj = zlib.decompressobj() + data = zobj.decompress(container_data) else: # Unknown compression method LOG.warning("Unknown compression method (%d)", method) From db177b386e04d6369ea99949a0f1145e58e21868 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 17 Sep 2023 22:06:40 +0200 Subject: [PATCH 34/46] Relax BitTiming & BitTimingFd Validation (#1618) * add `strict` parameter to can.BitTiming * add `strict` parameter to can.BitTimingFd * use `strict` in `from_bitrate_and_segments` and `recreate_with_f_clock` * cover `strict` parameter in tests * pylint: disable=too-many-arguments --- can/bit_timing.py | 160 ++++++++++++++++++++++++++-------------- can/util.py | 6 +- test/test_bit_timing.py | 73 ++++++++++++++++-- test/test_util.py | 12 +-- 4 files changed, 181 insertions(+), 70 deletions(-) diff --git a/can/bit_timing.py b/can/bit_timing.py index bf76c08af..285b1c302 100644 --- a/can/bit_timing.py +++ b/can/bit_timing.py @@ -36,6 +36,7 @@ def __init__( tseg2: int, sjw: int, nof_samples: int = 1, + strict: bool = False, ) -> None: """ :param int f_clock: @@ -56,6 +57,10 @@ def __init__( In this case, the bit will be sampled three quanta in a row, with the last sample being taken in the edge between TSEG1 and TSEG2. Three samples should only be used for relatively slow baudrates. + :param bool strict: + If True, restrict bit timings to the minimum required range as defined in + ISO 11898. This can be used to ensure compatibility across a wide variety + of CAN hardware. :raises ValueError: if the arguments are invalid. """ @@ -68,19 +73,13 @@ def __init__( "nof_samples": nof_samples, } self._validate() + if strict: + self._restrict_to_minimum_range() def _validate(self) -> None: - if not 8 <= self.nbt <= 25: - raise ValueError(f"nominal bit time (={self.nbt}) must be in [8...25].") - if not 1 <= self.brp <= 64: raise ValueError(f"bitrate prescaler (={self.brp}) must be in [1...64].") - if not 5_000 <= self.bitrate <= 2_000_000: - raise ValueError( - f"bitrate (={self.bitrate}) must be in [5,000...2,000,000]." - ) - if not 1 <= self.tseg1 <= 16: raise ValueError(f"tseg1 (={self.tseg1}) must be in [1...16].") @@ -104,6 +103,18 @@ def _validate(self) -> None: if self.nof_samples not in (1, 3): raise ValueError("nof_samples must be 1 or 3") + def _restrict_to_minimum_range(self) -> None: + if not 8 <= self.nbt <= 25: + raise ValueError(f"nominal bit time (={self.nbt}) must be in [8...25].") + + if not 1 <= self.brp <= 32: + raise ValueError(f"bitrate prescaler (={self.brp}) must be in [1...32].") + + if not 5_000 <= self.bitrate <= 1_000_000: + raise ValueError( + f"bitrate (={self.bitrate}) must be in [5,000...1,000,000]." + ) + @classmethod def from_bitrate_and_segments( cls, @@ -113,6 +124,7 @@ def from_bitrate_and_segments( tseg2: int, sjw: int, nof_samples: int = 1, + strict: bool = False, ) -> "BitTiming": """Create a :class:`~can.BitTiming` instance from bitrate and segment lengths. @@ -134,6 +146,10 @@ def from_bitrate_and_segments( In this case, the bit will be sampled three quanta in a row, with the last sample being taken in the edge between TSEG1 and TSEG2. Three samples should only be used for relatively slow baudrates. + :param bool strict: + If True, restrict bit timings to the minimum required range as defined in + ISO 11898. This can be used to ensure compatibility across a wide variety + of CAN hardware. :raises ValueError: if the arguments are invalid. """ @@ -149,6 +165,7 @@ def from_bitrate_and_segments( tseg2=tseg2, sjw=sjw, nof_samples=nof_samples, + strict=strict, ) if abs(bt.bitrate - bitrate) > bitrate / 256: raise ValueError( @@ -175,6 +192,11 @@ def from_registers( :raises ValueError: if the arguments are invalid. """ + if not 0 <= btr0 < 2**16: + raise ValueError(f"Invalid btr0 value. ({btr0})") + if not 0 <= btr1 < 2**16: + raise ValueError(f"Invalid btr1 value. ({btr1})") + brp = (btr0 & 0x3F) + 1 sjw = (btr0 >> 6) + 1 tseg1 = (btr1 & 0xF) + 1 @@ -239,6 +261,7 @@ def from_sample_point( tseg1=tseg1, tseg2=tseg2, sjw=sjw, + strict=True, ) possible_solutions.append(bt) except ValueError: @@ -316,12 +339,12 @@ def sample_point(self) -> float: @property def btr0(self) -> int: - """Bit timing register 0.""" + """Bit timing register 0 for SJA1000.""" return (self.sjw - 1) << 6 | self.brp - 1 @property def btr1(self) -> int: - """Bit timing register 1.""" + """Bit timing register 1 for SJA1000.""" sam = 1 if self.nof_samples == 3 else 0 return sam << 7 | (self.tseg2 - 1) << 4 | self.tseg1 - 1 @@ -373,6 +396,7 @@ def recreate_with_f_clock(self, f_clock: int) -> "BitTiming": tseg2=self.tseg2, sjw=self.sjw, nof_samples=self.nof_samples, + strict=True, ) except ValueError: pass @@ -474,7 +498,7 @@ class BitTimingFd(Mapping): ) """ - def __init__( + def __init__( # pylint: disable=too-many-arguments self, f_clock: int, nom_brp: int, @@ -485,6 +509,7 @@ def __init__( data_tseg1: int, data_tseg2: int, data_sjw: int, + strict: bool = False, ) -> None: """ Initialize a BitTimingFd instance with the specified parameters. @@ -513,6 +538,10 @@ def __init__( :param int data_sjw: The Synchronization Jump Width for the data phase. This value determines the maximum number of time quanta that the controller can resynchronize every bit. + :param bool strict: + If True, restrict bit timings to the minimum required range as defined in + ISO 11898. This can be used to ensure compatibility across a wide variety + of CAN hardware. :raises ValueError: if the arguments are invalid. """ @@ -528,32 +557,23 @@ def __init__( "data_sjw": data_sjw, } self._validate() + if strict: + self._restrict_to_minimum_range() def _validate(self) -> None: - if self.nbt < 8: - raise ValueError(f"nominal bit time (={self.nbt}) must be at least 8.") - - if self.dbt < 8: - raise ValueError(f"data bit time (={self.dbt}) must be at least 8.") - - if not 1 <= self.nom_brp <= 256: - raise ValueError( - f"nominal bitrate prescaler (={self.nom_brp}) must be in [1...256]." - ) - - if not 1 <= self.data_brp <= 256: - raise ValueError( - f"data bitrate prescaler (={self.data_brp}) must be in [1...256]." - ) + for param, value in self._data.items(): + if value < 0: # type: ignore[operator] + err_msg = f"'{param}' (={value}) must not be negative." + raise ValueError(err_msg) - if not 5_000 <= self.nom_bitrate <= 2_000_000: + if self.nom_brp < 1: raise ValueError( - f"nom_bitrate (={self.nom_bitrate}) must be in [5,000...2,000,000]." + f"nominal bitrate prescaler (={self.nom_brp}) must be at least 1." ) - if not 25_000 <= self.data_bitrate <= 8_000_000: + if self.data_brp < 1: raise ValueError( - f"data_bitrate (={self.data_bitrate}) must be in [25,000...8,000,000]." + f"data bitrate prescaler (={self.data_brp}) must be at least 1." ) if self.data_bitrate < self.nom_bitrate: @@ -562,30 +582,12 @@ def _validate(self) -> None: f"equal to nom_bitrate (={self.nom_bitrate})" ) - if not 2 <= self.nom_tseg1 <= 256: - raise ValueError(f"nom_tseg1 (={self.nom_tseg1}) must be in [2...256].") - - if not 1 <= self.nom_tseg2 <= 128: - raise ValueError(f"nom_tseg2 (={self.nom_tseg2}) must be in [1...128].") - - if not 1 <= self.data_tseg1 <= 32: - raise ValueError(f"data_tseg1 (={self.data_tseg1}) must be in [1...32].") - - if not 1 <= self.data_tseg2 <= 16: - raise ValueError(f"data_tseg2 (={self.data_tseg2}) must be in [1...16].") - - if not 1 <= self.nom_sjw <= 128: - raise ValueError(f"nom_sjw (={self.nom_sjw}) must be in [1...128].") - if self.nom_sjw > self.nom_tseg2: raise ValueError( f"nom_sjw (={self.nom_sjw}) must not be " f"greater than nom_tseg2 (={self.nom_tseg2})." ) - if not 1 <= self.data_sjw <= 16: - raise ValueError(f"data_sjw (={self.data_sjw}) must be in [1...128].") - if self.data_sjw > self.data_tseg2: raise ValueError( f"data_sjw (={self.data_sjw}) must not be " @@ -604,8 +606,46 @@ def _validate(self) -> None: f"(data_sample_point={self.data_sample_point:.2f}%)." ) + def _restrict_to_minimum_range(self) -> None: + # restrict to minimum required range as defined in ISO 11898 + if not 8 <= self.nbt <= 80: + raise ValueError(f"Nominal bit time (={self.nbt}) must be in [8...80]") + + if not 5 <= self.dbt <= 25: + raise ValueError(f"Nominal bit time (={self.dbt}) must be in [5...25]") + + if not 1 <= self.data_tseg1 <= 16: + raise ValueError(f"data_tseg1 (={self.data_tseg1}) must be in [1...16].") + + if not 2 <= self.data_tseg2 <= 8: + raise ValueError(f"data_tseg2 (={self.data_tseg2}) must be in [2...8].") + + if not 1 <= self.data_sjw <= 8: + raise ValueError(f"data_sjw (={self.data_sjw}) must be in [1...8].") + + if self.nom_brp == self.data_brp: + # shared prescaler + if not 2 <= self.nom_tseg1 <= 128: + raise ValueError(f"nom_tseg1 (={self.nom_tseg1}) must be in [2...128].") + + if not 2 <= self.nom_tseg2 <= 32: + raise ValueError(f"nom_tseg2 (={self.nom_tseg2}) must be in [2...32].") + + if not 1 <= self.nom_sjw <= 32: + raise ValueError(f"nom_sjw (={self.nom_sjw}) must be in [1...32].") + else: + # separate prescaler + if not 2 <= self.nom_tseg1 <= 64: + raise ValueError(f"nom_tseg1 (={self.nom_tseg1}) must be in [2...64].") + + if not 2 <= self.nom_tseg2 <= 16: + raise ValueError(f"nom_tseg2 (={self.nom_tseg2}) must be in [2...16].") + + if not 1 <= self.nom_sjw <= 16: + raise ValueError(f"nom_sjw (={self.nom_sjw}) must be in [1...16].") + @classmethod - def from_bitrate_and_segments( + def from_bitrate_and_segments( # pylint: disable=too-many-arguments cls, f_clock: int, nom_bitrate: int, @@ -616,6 +656,7 @@ def from_bitrate_and_segments( data_tseg1: int, data_tseg2: int, data_sjw: int, + strict: bool = False, ) -> "BitTimingFd": """ Create a :class:`~can.BitTimingFd` instance with the bitrates and segments lengths. @@ -644,6 +685,10 @@ def from_bitrate_and_segments( :param int data_sjw: The Synchronization Jump Width for the data phase. This value determines the maximum number of time quanta that the controller can resynchronize every bit. + :param bool strict: + If True, restrict bit timings to the minimum required range as defined in + ISO 11898. This can be used to ensure compatibility across a wide variety + of CAN hardware. :raises ValueError: if the arguments are invalid. """ @@ -665,6 +710,7 @@ def from_bitrate_and_segments( data_tseg1=data_tseg1, data_tseg2=data_tseg2, data_sjw=data_sjw, + strict=strict, ) if abs(bt.nom_bitrate - nom_bitrate) > nom_bitrate / 256: @@ -724,9 +770,11 @@ def from_sample_point( possible_solutions: List[BitTimingFd] = [] + sync_seg = 1 + for nom_brp in range(1, 257): nbt = round(int(f_clock / (nom_bitrate * nom_brp))) - if nbt < 8: + if nbt < 1: break effective_nom_bitrate = f_clock / (nbt * nom_brp) @@ -734,15 +782,15 @@ def from_sample_point( continue nom_tseg1 = int(round(nom_sample_point / 100 * nbt)) - 1 - # limit tseg1, so tseg2 is at least 1 TQ - nom_tseg1 = min(nom_tseg1, nbt - 2) + # limit tseg1, so tseg2 is at least 2 TQ + nom_tseg1 = min(nom_tseg1, nbt - sync_seg - 2) nom_tseg2 = nbt - nom_tseg1 - 1 nom_sjw = min(nom_tseg2, 128) for data_brp in range(1, 257): dbt = round(int(f_clock / (data_bitrate * data_brp))) - if dbt < 8: + if dbt < 1: break effective_data_bitrate = f_clock / (dbt * data_brp) @@ -750,8 +798,8 @@ def from_sample_point( continue data_tseg1 = int(round(data_sample_point / 100 * dbt)) - 1 - # limit tseg1, so tseg2 is at least 1 TQ - data_tseg1 = min(data_tseg1, dbt - 2) + # limit tseg1, so tseg2 is at least 2 TQ + data_tseg1 = min(data_tseg1, dbt - sync_seg - 2) data_tseg2 = dbt - data_tseg1 - 1 data_sjw = min(data_tseg2, 16) @@ -767,6 +815,7 @@ def from_sample_point( data_tseg1=data_tseg1, data_tseg2=data_tseg2, data_sjw=data_sjw, + strict=True, ) possible_solutions.append(bt) except ValueError: @@ -971,6 +1020,7 @@ def recreate_with_f_clock(self, f_clock: int) -> "BitTimingFd": data_tseg1=self.data_tseg1, data_tseg2=self.data_tseg2, data_sjw=self.data_sjw, + strict=True, ) except ValueError: pass diff --git a/can/util.py b/can/util.py index 71980e82f..402934379 100644 --- a/can/util.py +++ b/can/util.py @@ -250,14 +250,16 @@ def _create_bus_config(config: Dict[str, Any]) -> typechecking.BusConfig: **{ key: int(config[key]) for key in typechecking.BitTimingFdDict.__annotations__ - } + }, + strict=False, ) elif set(typechecking.BitTimingDict.__annotations__).issubset(config): config["timing"] = can.BitTiming( **{ key: int(config[key]) for key in typechecking.BitTimingDict.__annotations__ - } + }, + strict=False, ) except (ValueError, TypeError): pass diff --git a/test/test_bit_timing.py b/test/test_bit_timing.py index d0d0a4a7f..6852687a5 100644 --- a/test/test_bit_timing.py +++ b/test/test_bit_timing.py @@ -11,7 +11,7 @@ def test_sja1000(): """Test some values obtained using other bit timing calculators.""" timing = can.BitTiming( - f_clock=8_000_000, brp=4, tseg1=11, tseg2=4, sjw=2, nof_samples=3 + f_clock=8_000_000, brp=4, tseg1=11, tseg2=4, sjw=2, nof_samples=3, strict=True ) assert timing.f_clock == 8_000_000 assert timing.bitrate == 125_000 @@ -25,7 +25,9 @@ def test_sja1000(): assert timing.btr0 == 0x43 assert timing.btr1 == 0xBA - timing = can.BitTiming(f_clock=8_000_000, brp=1, tseg1=13, tseg2=2, sjw=1) + timing = can.BitTiming( + f_clock=8_000_000, brp=1, tseg1=13, tseg2=2, sjw=1, strict=True + ) assert timing.f_clock == 8_000_000 assert timing.bitrate == 500_000 assert timing.brp == 1 @@ -38,7 +40,9 @@ def test_sja1000(): assert timing.btr0 == 0x00 assert timing.btr1 == 0x1C - timing = can.BitTiming(f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1) + timing = can.BitTiming( + f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1, strict=True + ) assert timing.f_clock == 8_000_000 assert timing.bitrate == 1_000_000 assert timing.brp == 1 @@ -84,7 +88,7 @@ def test_from_bitrate_and_segments(): assert timing.btr1 == 0x1C timing = can.BitTiming.from_bitrate_and_segments( - f_clock=8_000_000, bitrate=1_000_000, tseg1=5, tseg2=2, sjw=1 + f_clock=8_000_000, bitrate=1_000_000, tseg1=5, tseg2=2, sjw=1, strict=True ) assert timing.f_clock == 8_000_000 assert timing.bitrate == 1_000_000 @@ -127,8 +131,24 @@ def test_from_bitrate_and_segments(): assert timing.data_sjw == 10 assert timing.data_sample_point == 75 + # test strict invalid + with pytest.raises(ValueError): + can.BitTimingFd.from_bitrate_and_segments( + f_clock=80_000_000, + nom_bitrate=500_000, + nom_tseg1=119, + nom_tseg2=40, + nom_sjw=40, + data_bitrate=2_000_000, + data_tseg1=29, + data_tseg2=10, + data_sjw=10, + strict=True, + ) + def test_can_fd(): + # test non-strict timing = can.BitTimingFd( f_clock=80_000_000, nom_brp=1, @@ -149,7 +169,6 @@ def test_can_fd(): assert timing.nom_tseg2 == 40 assert timing.nom_sjw == 40 assert timing.nom_sample_point == 75 - assert timing.f_clock == 80_000_000 assert timing.data_bitrate == 2_000_000 assert timing.data_brp == 1 assert timing.dbt == 40 @@ -158,6 +177,50 @@ def test_can_fd(): assert timing.data_sjw == 10 assert timing.data_sample_point == 75 + # test strict invalid + with pytest.raises(ValueError): + can.BitTimingFd( + f_clock=80_000_000, + nom_brp=1, + nom_tseg1=119, + nom_tseg2=40, + nom_sjw=40, + data_brp=1, + data_tseg1=29, + data_tseg2=10, + data_sjw=10, + strict=True, + ) + + # test strict valid + timing = can.BitTimingFd( + f_clock=80_000_000, + nom_brp=2, + nom_tseg1=59, + nom_tseg2=20, + nom_sjw=20, + data_brp=2, + data_tseg1=14, + data_tseg2=5, + data_sjw=5, + strict=True, + ) + assert timing.f_clock == 80_000_000 + assert timing.nom_bitrate == 500_000 + assert timing.nom_brp == 2 + assert timing.nbt == 80 + assert timing.nom_tseg1 == 59 + assert timing.nom_tseg2 == 20 + assert timing.nom_sjw == 20 + assert timing.nom_sample_point == 75 + assert timing.data_bitrate == 2_000_000 + assert timing.data_brp == 2 + assert timing.dbt == 20 + assert timing.data_tseg1 == 14 + assert timing.data_tseg2 == 5 + assert timing.data_sjw == 5 + assert timing.data_sample_point == 75 + def test_from_btr(): timing = can.BitTiming.from_registers(f_clock=8_000_000, btr0=0x00, btr1=0x14) diff --git a/test/test_util.py b/test/test_util.py index a4aacdf86..ac8c87d9e 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -253,14 +253,10 @@ def test_adjust_timing_fd(self): ) assert new_timing.__class__ == BitTimingFd assert new_timing.f_clock == 80_000_000 - assert new_timing.nom_bitrate == 500_000 - assert new_timing.nom_tseg1 == 119 - assert new_timing.nom_tseg2 == 40 - assert new_timing.nom_sjw == 40 - assert new_timing.data_bitrate == 2_000_000 - assert new_timing.data_tseg1 == 29 - assert new_timing.data_tseg2 == 10 - assert new_timing.data_sjw == 10 + assert new_timing.nom_bitrate == timing.nom_bitrate + assert new_timing.nom_sample_point == timing.nom_sample_point + assert new_timing.data_bitrate == timing.data_bitrate + assert new_timing.data_sample_point == timing.data_sample_point with pytest.raises(CanInitializationError): check_or_adjust_timing_clock(timing, valid_clocks=[8_000, 16_000]) From 3c3f12313a18f714ebfa21b77fa30b87d4746e98 Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Mon, 2 Oct 2023 18:22:04 -0400 Subject: [PATCH 35/46] We do not need to account for drift when we USE_WINDOWS_EVENTS (#1666) --- can/broadcastmanager.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index a017b7d52..398114a59 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -35,7 +35,6 @@ log = logging.getLogger("can.bcm") NANOSECONDS_IN_SECOND: Final[int] = 1_000_000_000 -NANOSECONDS_IN_MILLISECOND: Final[int] = 1_000_000 class CyclicTask(abc.ABC): @@ -316,19 +315,19 @@ def _run(self) -> None: self.stop() break - msg_due_time_ns += self.period_ns + if not USE_WINDOWS_EVENTS: + msg_due_time_ns += self.period_ns if self.end_time is not None and time.perf_counter() >= self.end_time: break msg_index = (msg_index + 1) % len(self.messages) - # Compensate for the time it takes to send the message - delay_ns = msg_due_time_ns - time.perf_counter_ns() - - if delay_ns > 0: - if USE_WINDOWS_EVENTS: - win32event.WaitForSingleObject( - self.event.handle, - int(round(delay_ns / NANOSECONDS_IN_MILLISECOND)), - ) - else: + if USE_WINDOWS_EVENTS: + win32event.WaitForSingleObject( + self.event.handle, + win32event.INFINITE, + ) + else: + # Compensate for the time it takes to send the message + delay_ns = msg_due_time_ns - time.perf_counter_ns() + if delay_ns > 0: time.sleep(delay_ns / NANOSECONDS_IN_SECOND) From 237f2be345071325f3f94469b7d3a8912d21077d Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Mon, 9 Oct 2023 00:02:21 +0200 Subject: [PATCH 36/46] Update linters, activate more ruff rules (#1669) * update ruff to 0.0.286 * activate pyflakes rules * activate flake8-type-checking * activate Ruff-specific rules * activate pylint rules * activate flake8-comprehensions * update ruff to 0.0.292 * remove unnecessary tuple() call * update mypy to 1.5.*, update black to 23.9.* --------- Co-authored-by: zariiii9003 --- can/bit_timing.py | 5 +- can/interfaces/gs_usb.py | 2 +- can/interfaces/ics_neovi/neovi_bus.py | 18 ++-- can/interfaces/ixxat/canlib.py | 18 ++-- can/interfaces/ixxat/canlib_vcinpl2.py | 14 ++-- can/interfaces/pcan/basic.py | 104 +++++++++++------------- can/interfaces/pcan/pcan.py | 6 +- can/interfaces/socketcand/socketcand.py | 2 +- can/interfaces/systec/ucan.py | 2 +- can/interfaces/vector/canlib.py | 18 ++-- can/io/asc.py | 8 +- can/io/trc.py | 4 +- can/logger.py | 8 +- can/util.py | 5 +- can/viewer.py | 2 +- pyproject.toml | 28 ++++--- test/back2back_test.py | 2 +- test/network_test.py | 7 +- test/test_player.py | 10 +-- test/test_slcan.py | 4 +- test/test_socketcan.py | 4 +- test/test_vector.py | 2 +- 22 files changed, 133 insertions(+), 140 deletions(-) diff --git a/can/bit_timing.py b/can/bit_timing.py index 285b1c302..3e8cb1bcd 100644 --- a/can/bit_timing.py +++ b/can/bit_timing.py @@ -1,8 +1,9 @@ # pylint: disable=too-many-lines import math -from typing import Iterator, List, Mapping, cast +from typing import TYPE_CHECKING, Iterator, List, Mapping, cast -from can.typechecking import BitTimingDict, BitTimingFdDict +if TYPE_CHECKING: + from can.typechecking import BitTimingDict, BitTimingFdDict class BitTiming(Mapping): diff --git a/can/interfaces/gs_usb.py b/can/interfaces/gs_usb.py index 32ad54e75..38f9fe41a 100644 --- a/can/interfaces/gs_usb.py +++ b/can/interfaces/gs_usb.py @@ -35,7 +35,7 @@ def __init__( """ if (index is not None) and ((bus or address) is not None): raise CanInitializationError( - f"index and bus/address cannot be used simultaneously" + "index and bus/address cannot be used simultaneously" ) if index is not None: diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 0698c1416..9270bfc90 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -302,15 +302,15 @@ def _find_device(self, type_filter=None, serial=None): for device in devices: if serial is None or self.get_serial_number(device) == str(serial): return device - else: - msg = ["No device"] - - if type_filter is not None: - msg.append(f"with type {type_filter}") - if serial is not None: - msg.append(f"with serial {serial}") - msg.append("found.") - raise CanInitializationError(" ".join(msg)) + + msg = ["No device"] + + if type_filter is not None: + msg.append(f"with type {type_filter}") + if serial is not None: + msg.append(f"with serial {serial}") + msg.append("found.") + raise CanInitializationError(" ".join(msg)) def _process_msg_queue(self, timeout=0.1): try: diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 8c07508e4..f18c86acd 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -28,17 +28,17 @@ def __init__( unique_hardware_id: Optional[int] = None, extended: bool = True, fd: bool = False, - rx_fifo_size: int = None, - tx_fifo_size: int = None, + rx_fifo_size: Optional[int] = None, + tx_fifo_size: Optional[int] = None, bitrate: int = 500000, data_bitrate: int = 2000000, - sjw_abr: int = None, - tseg1_abr: int = None, - tseg2_abr: int = None, - sjw_dbr: int = None, - tseg1_dbr: int = None, - tseg2_dbr: int = None, - ssp_dbr: int = None, + sjw_abr: Optional[int] = None, + tseg1_abr: Optional[int] = None, + tseg2_abr: Optional[int] = None, + sjw_dbr: Optional[int] = None, + tseg1_dbr: Optional[int] = None, + tseg2_dbr: Optional[int] = None, + ssp_dbr: Optional[int] = None, **kwargs, ): """ diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index b10ac1b94..2c306c880 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -436,13 +436,13 @@ def __init__( tx_fifo_size: int = 128, bitrate: int = 500000, data_bitrate: int = 2000000, - sjw_abr: int = None, - tseg1_abr: int = None, - tseg2_abr: int = None, - sjw_dbr: int = None, - tseg1_dbr: int = None, - tseg2_dbr: int = None, - ssp_dbr: int = None, + sjw_abr: Optional[int] = None, + tseg1_abr: Optional[int] = None, + tseg2_abr: Optional[int] = None, + sjw_dbr: Optional[int] = None, + tseg1_dbr: Optional[int] = None, + tseg2_dbr: Optional[int] = None, + ssp_dbr: Optional[int] = None, **kwargs, ): """ diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index 7c41e2816..a57340955 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -297,73 +297,63 @@ # PCAN parameter values # -PCAN_PARAMETER_OFF = int(0x00) # The PCAN parameter is not set (inactive) -PCAN_PARAMETER_ON = int(0x01) # The PCAN parameter is set (active) -PCAN_FILTER_CLOSE = int(0x00) # The PCAN filter is closed. No messages will be received -PCAN_FILTER_OPEN = int( - 0x01 -) # The PCAN filter is fully opened. All messages will be received -PCAN_FILTER_CUSTOM = int( - 0x02 -) # The PCAN filter is custom configured. Only registered messages will be received -PCAN_CHANNEL_UNAVAILABLE = int( - 0x00 -) # The PCAN-Channel handle is illegal, or its associated hardware is not available -PCAN_CHANNEL_AVAILABLE = int( - 0x01 -) # The PCAN-Channel handle is available to be connected (PnP Hardware: it means furthermore that the hardware is plugged-in) -PCAN_CHANNEL_OCCUPIED = int( - 0x02 -) # The PCAN-Channel handle is valid, and is already being used +PCAN_PARAMETER_OFF = 0x00 # The PCAN parameter is not set (inactive) +PCAN_PARAMETER_ON = 0x01 # The PCAN parameter is set (active) +PCAN_FILTER_CLOSE = 0x00 # The PCAN filter is closed. No messages will be received +PCAN_FILTER_OPEN = ( + 0x01 # The PCAN filter is fully opened. All messages will be received +) +PCAN_FILTER_CUSTOM = 0x02 # The PCAN filter is custom configured. Only registered messages will be received +PCAN_CHANNEL_UNAVAILABLE = 0x00 # The PCAN-Channel handle is illegal, or its associated hardware is not available +PCAN_CHANNEL_AVAILABLE = 0x01 # The PCAN-Channel handle is available to be connected (PnP Hardware: it means furthermore that the hardware is plugged-in) +PCAN_CHANNEL_OCCUPIED = ( + 0x02 # The PCAN-Channel handle is valid, and is already being used +) PCAN_CHANNEL_PCANVIEW = ( PCAN_CHANNEL_AVAILABLE | PCAN_CHANNEL_OCCUPIED ) # The PCAN-Channel handle is already being used by a PCAN-View application, but is available to connect -LOG_FUNCTION_DEFAULT = int(0x00) # Logs system exceptions / errors -LOG_FUNCTION_ENTRY = int(0x01) # Logs the entries to the PCAN-Basic API functions -LOG_FUNCTION_PARAMETERS = int( - 0x02 -) # Logs the parameters passed to the PCAN-Basic API functions -LOG_FUNCTION_LEAVE = int(0x04) # Logs the exits from the PCAN-Basic API functions -LOG_FUNCTION_WRITE = int(0x08) # Logs the CAN messages passed to the CAN_Write function -LOG_FUNCTION_READ = int( - 0x10 -) # Logs the CAN messages received within the CAN_Read function -LOG_FUNCTION_ALL = int( - 0xFFFF -) # Logs all possible information within the PCAN-Basic API functions +LOG_FUNCTION_DEFAULT = 0x00 # Logs system exceptions / errors +LOG_FUNCTION_ENTRY = 0x01 # Logs the entries to the PCAN-Basic API functions +LOG_FUNCTION_PARAMETERS = ( + 0x02 # Logs the parameters passed to the PCAN-Basic API functions +) +LOG_FUNCTION_LEAVE = 0x04 # Logs the exits from the PCAN-Basic API functions +LOG_FUNCTION_WRITE = 0x08 # Logs the CAN messages passed to the CAN_Write function +LOG_FUNCTION_READ = 0x10 # Logs the CAN messages received within the CAN_Read function +LOG_FUNCTION_ALL = ( + 0xFFFF # Logs all possible information within the PCAN-Basic API functions +) -TRACE_FILE_SINGLE = int( - 0x00 -) # A single file is written until it size reaches PAN_TRACE_SIZE -TRACE_FILE_SEGMENTED = int( - 0x01 -) # Traced data is distributed in several files with size PAN_TRACE_SIZE -TRACE_FILE_DATE = int(0x02) # Includes the date into the name of the trace file -TRACE_FILE_TIME = int(0x04) # Includes the start time into the name of the trace file -TRACE_FILE_OVERWRITE = int( - 0x80 -) # Causes the overwriting of available traces (same name) +TRACE_FILE_SINGLE = ( + 0x00 # A single file is written until it size reaches PAN_TRACE_SIZE +) +TRACE_FILE_SEGMENTED = ( + 0x01 # Traced data is distributed in several files with size PAN_TRACE_SIZE +) +TRACE_FILE_DATE = 0x02 # Includes the date into the name of the trace file +TRACE_FILE_TIME = 0x04 # Includes the start time into the name of the trace file +TRACE_FILE_OVERWRITE = 0x80 # Causes the overwriting of available traces (same name) -FEATURE_FD_CAPABLE = int(0x01) # Device supports flexible data-rate (CAN-FD) -FEATURE_DELAY_CAPABLE = int( - 0x02 -) # Device supports a delay between sending frames (FPGA based USB devices) -FEATURE_IO_CAPABLE = int( - 0x04 -) # Device supports I/O functionality for electronic circuits (USB-Chip devices) +FEATURE_FD_CAPABLE = 0x01 # Device supports flexible data-rate (CAN-FD) +FEATURE_DELAY_CAPABLE = ( + 0x02 # Device supports a delay between sending frames (FPGA based USB devices) +) +FEATURE_IO_CAPABLE = ( + 0x04 # Device supports I/O functionality for electronic circuits (USB-Chip devices) +) -SERVICE_STATUS_STOPPED = int(0x01) # The service is not running -SERVICE_STATUS_RUNNING = int(0x04) # The service is running +SERVICE_STATUS_STOPPED = 0x01 # The service is not running +SERVICE_STATUS_RUNNING = 0x04 # The service is running # Other constants # -MAX_LENGTH_HARDWARE_NAME = int( - 33 -) # Maximum length of the name of a device: 32 characters + terminator -MAX_LENGTH_VERSION_STRING = int( - 256 -) # Maximum length of a version string: 255 characters + terminator +MAX_LENGTH_HARDWARE_NAME = ( + 33 # Maximum length of the name of a device: 32 characters + terminator +) +MAX_LENGTH_VERSION_STRING = ( + 256 # Maximum length of a version string: 255 characters + terminator +) # PCAN message types # diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 25610a614..01a4b1dc3 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -85,7 +85,7 @@ boottimeEpoch = 0 else: boottimeEpoch = uptime.boottime().timestamp() -except ImportError as error: +except ImportError: log.warning( "uptime library not available, timestamps are relative to boot time and not to Epoch UTC", ) @@ -283,7 +283,7 @@ def __init__( clock_param = "f_clock" if "f_clock" in kwargs else "f_clock_mhz" fd_parameters_values = [ f"{key}={kwargs[key]}" - for key in (clock_param,) + PCAN_FD_PARAMETER_LIST + for key in (clock_param, *PCAN_FD_PARAMETER_LIST) if key in kwargs ] @@ -413,7 +413,7 @@ def bits(n): def get_api_version(self): error, value = self.m_objPCANBasic.GetValue(PCAN_NONEBUS, PCAN_API_VERSION) if error != PCAN_ERROR_OK: - raise CanInitializationError(f"Failed to read pcan basic api version") + raise CanInitializationError("Failed to read pcan basic api version") # fix https://github.com/hardbyte/python-can/issues/1642 version_string = value.decode("ascii").replace(",", ".").replace(" ", "") diff --git a/can/interfaces/socketcand/socketcand.py b/can/interfaces/socketcand/socketcand.py index 183a9ba12..26ae63ca6 100644 --- a/can/interfaces/socketcand/socketcand.py +++ b/can/interfaces/socketcand/socketcand.py @@ -91,7 +91,7 @@ def __init__(self, channel, host, port, can_filters=None, **kwargs): ) self._tcp_send(f"< open {channel} >") self._expect_msg("< ok >") - self._tcp_send(f"< rawmode >") + self._tcp_send("< rawmode >") self._expect_msg("< ok >") super().__init__(channel=channel, can_filters=can_filters, **kwargs) diff --git a/can/interfaces/systec/ucan.py b/can/interfaces/systec/ucan.py index bbc484314..f969532d7 100644 --- a/can/interfaces/systec/ucan.py +++ b/can/interfaces/systec/ucan.py @@ -409,7 +409,7 @@ def init_hardware(self, serial=None, device_number=ANY_MODULE): Initializes the device with the corresponding serial or device number. :param int or None serial: Serial number of the USB-CANmodul. - :param int device_number: Device number (0 – 254, or :const:`ANY_MODULE` for the first device). + :param int device_number: Device number (0 - 254, or :const:`ANY_MODULE` for the first device). """ if not self._hw_is_initialized: # initialize hardware either by device number or serial diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 55aeb5da4..cedf25666 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -69,19 +69,17 @@ class VectorBus(BusABC): """The CAN Bus implemented for the Vector interface.""" - deprecated_args = dict( - sjwAbr="sjw_abr", - tseg1Abr="tseg1_abr", - tseg2Abr="tseg2_abr", - sjwDbr="sjw_dbr", - tseg1Dbr="tseg1_dbr", - tseg2Dbr="tseg2_dbr", - ) - @deprecated_args_alias( deprecation_start="4.0.0", deprecation_end="5.0.0", - **deprecated_args, + **{ + "sjwAbr": "sjw_abr", + "tseg1Abr": "tseg1_abr", + "tseg2Abr": "tseg2_abr", + "sjwDbr": "sjw_dbr", + "tseg1Dbr": "tseg1_dbr", + "tseg2Dbr": "tseg2_dbr", + }, ) def __init__( self, diff --git a/can/io/asc.py b/can/io/asc.py index 3114acfbe..f039cda32 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -64,8 +64,8 @@ def __init__( self.internal_events_logged = False def _extract_header(self) -> None: - for line in self.file: - line = line.strip() + for _line in self.file: + line = _line.strip() datetime_match = re.match( r"date\s+\w+\s+(?P.+)", line, re.IGNORECASE @@ -255,8 +255,8 @@ def _process_fd_can_frame(self, line: str, msg_kwargs: Dict[str, Any]) -> Messag def __iter__(self) -> Generator[Message, None, None]: self._extract_header() - for line in self.file: - line = line.strip() + for _line in self.file: + line = _line.strip() trigger_match = re.match( r"begin\s+triggerblock\s+\w+\s+(?P.+)", diff --git a/can/io/trc.py b/can/io/trc.py index ccf122d57..889d55196 100644 --- a/can/io/trc.py +++ b/can/io/trc.py @@ -68,8 +68,8 @@ def __init__( def _extract_header(self): line = "" - for line in self.file: - line = line.strip() + for _line in self.file: + line = _line.strip() if line.startswith(";$FILEVERSION"): logger.debug("TRCReader: Found file version '%s'", line) try: diff --git a/can/logger.py b/can/logger.py index 56f9156a8..f20965b04 100644 --- a/can/logger.py +++ b/can/logger.py @@ -3,16 +3,18 @@ import re import sys from datetime import datetime -from typing import Any, Dict, List, Sequence, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, List, Sequence, Tuple, Union import can -from can.io import BaseRotatingLogger -from can.io.generic import MessageWriter from can.util import cast_from_string from . import Bus, BusState, Logger, SizedRotatingLogger from .typechecking import CanFilter, CanFilters +if TYPE_CHECKING: + from can.io import BaseRotatingLogger + from can.io.generic import MessageWriter + def _create_base_argument_parser(parser: argparse.ArgumentParser) -> None: """Adds common options to an argument parser.""" diff --git a/can/util.py b/can/util.py index 402934379..42b1f49ac 100644 --- a/can/util.py +++ b/can/util.py @@ -190,9 +190,8 @@ def load_config( ) # Slightly complex here to only search for the file config if required - for cfg in config_sources: - if callable(cfg): - cfg = cfg(context) + for _cfg in config_sources: + cfg = _cfg(context) if callable(_cfg) else _cfg # remove legacy operator (and copy to interface if not already present) if "bustype" in cfg: if "interface" not in cfg or not cfg["interface"]: diff --git a/can/viewer.py b/can/viewer.py index be7f76b73..db19fd1f6 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -537,7 +537,7 @@ def parse_args(args: List[str]) -> Tuple: scaling.append(float(t)) if scaling: - data_structs[key] = (struct.Struct(fmt),) + tuple(scaling) + data_structs[key] = (struct.Struct(fmt), *scaling) else: data_structs[key] = struct.Struct(fmt) diff --git a/pyproject.toml b/pyproject.toml index 6ee1dbadc..e20fd2785 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,9 +59,9 @@ changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] lint = [ "pylint==2.17.*", - "ruff==0.0.269", - "black==23.3.*", - "mypy==1.3.*", + "ruff==0.0.292", + "black==23.9.*", + "mypy==1.5.*", ] seeedstudio = ["pyserial>=3.0"] serial = ["pyserial~=3.0"] @@ -91,7 +91,7 @@ examples = ["*.py"] can = ["py.typed"] [tool.setuptools.packages.find] -include = ["can*", "scripts"] +include = ["can*"] [tool.mypy] warn_return_any = true @@ -128,14 +128,22 @@ exclude = [ [tool.ruff] select = [ - "F401", # unused-imports - "UP", # pyupgrade - "I", # isort - "E", # pycodestyle errors - "W", # pycodestyle warnings + "F", # pyflakes + "UP", # pyupgrade + "I", # isort + "E", # pycodestyle errors + "W", # pycodestyle warnings + "PL", # pylint + "RUF", # ruff-specific rules + "C4", # flake8-comprehensions + "TCH", # flake8-type-checking ] ignore = [ - "E501", # Line too long + "E501", # Line too long + "F403", # undefined-local-with-import-star + "F405", # undefined-local-with-import-star-usage + "PLR", # pylint refactor + "RUF012", # mutable-class-default ] [tool.ruff.isort] diff --git a/test/back2back_test.py b/test/back2back_test.py index 52bfaf716..cd5aca6aa 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -116,7 +116,7 @@ def test_timestamp(self): self.assertTrue( 1.75 <= delta_time <= 2.25, "Time difference should have been 2s +/- 250ms." - "But measured {}".format(delta_time), + f"But measured {delta_time}", ) def test_standard_message(self): diff --git a/test/network_test.py b/test/network_test.py index 61690c1b4..250976fb2 100644 --- a/test/network_test.py +++ b/test/network_test.py @@ -6,12 +6,15 @@ import threading import unittest +import can + logging.getLogger(__file__).setLevel(logging.WARNING) + # make a random bool: -rbool = lambda: bool(round(random.random())) +def rbool(): + return bool(round(random.random())) -import can channel = "vcan0" diff --git a/test/test_player.py b/test/test_player.py index 5ad6e774c..e5e77fe8a 100755 --- a/test/test_player.py +++ b/test/test_player.py @@ -60,14 +60,8 @@ def test_play_virtual(self): dlc=8, data=[0x5, 0xC, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0], ) - if sys.version_info >= (3, 8): - # The args argument was introduced with python 3.8 - self.assertTrue( - msg1.equals(self.mock_virtual_bus.send.mock_calls[0].args[0]) - ) - self.assertTrue( - msg2.equals(self.mock_virtual_bus.send.mock_calls[1].args[0]) - ) + self.assertTrue(msg1.equals(self.mock_virtual_bus.send.mock_calls[0].args[0])) + self.assertTrue(msg2.equals(self.mock_virtual_bus.send.mock_calls[1].args[0])) self.assertSuccessfulCleanup() def test_play_virtual_verbose(self): diff --git a/test/test_slcan.py b/test/test_slcan.py index a2f8f5d15..f74207b9f 100644 --- a/test/test_slcan.py +++ b/test/test_slcan.py @@ -12,8 +12,8 @@ """ Mentioned in #1010 & #1490 -> PyPy works best with pure Python applications. Whenever you use a C extension module, -> it runs much slower than in CPython. The reason is that PyPy can't optimize C extension modules since they're not fully supported. +> PyPy works best with pure Python applications. Whenever you use a C extension module, +> it runs much slower than in CPython. The reason is that PyPy can't optimize C extension modules since they're not fully supported. > In addition, PyPy has to emulate reference counting for that part of the code, making it even slower. https://realpython.com/pypy-faster-python/#it-doesnt-work-well-with-c-extensions diff --git a/test/test_socketcan.py b/test/test_socketcan.py index f756cb93a..af06b8169 100644 --- a/test/test_socketcan.py +++ b/test/test_socketcan.py @@ -9,8 +9,6 @@ import warnings from unittest.mock import patch -from .config import TEST_INTERFACE_SOCKETCAN - import can from can.interfaces.socketcan.constants import ( CAN_BCM_TX_DELETE, @@ -28,7 +26,7 @@ build_bcm_update_header, ) -from .config import IS_LINUX, IS_PYPY +from .config import IS_LINUX, IS_PYPY, TEST_INTERFACE_SOCKETCAN class SocketCANTest(unittest.TestCase): diff --git a/test/test_vector.py b/test/test_vector.py index 93aba9c7b..ab9f8a928 100644 --- a/test/test_vector.py +++ b/test/test_vector.py @@ -881,7 +881,7 @@ def _find_xl_channel_config(serial: int, channel: int) -> xlclass.XLchannelConfi raise LookupError("XLchannelConfig not found.") -@functools.lru_cache() +@functools.lru_cache def _find_virtual_can_serial() -> int: """Serial number might be 0 or 100 depending on driver version.""" xl_driver_config = xlclass.XLdriverConfig() From e3d912b0bf5b5f21482d85041d1172af5b622188 Mon Sep 17 00:00:00 2001 From: Mateusz Dionizy <62315827+deronek@users.noreply.github.com> Date: Mon, 9 Oct 2023 00:06:17 +0200 Subject: [PATCH 37/46] Send HighPriority Message to flush VectorBus Tx buffer (#1636) * Refactor flush_tx_queue for VectorBus * Remove test_flush_tx_buffer_mocked Implementation cannot be tested due to Vector TX queue being not accessible * Update formatting in flush_tx_queue * Add tests for VectorBus flush_tx_queue * Refactor docstring for VectorBus flush_tx_queue --- can/interfaces/vector/canlib.py | 35 ++++++++++++++++++++++++++++++++- test/test_vector.py | 33 ++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index cedf25666..bc564958f 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -876,7 +876,40 @@ def _build_xl_can_tx_event(msg: Message) -> xlclass.XLcanTxEvent: return xl_can_tx_event def flush_tx_buffer(self) -> None: - self.xldriver.xlCanFlushTransmitQueue(self.port_handle, self.mask) + """ + Flush the TX buffer of the bus. + + Implementation does not use function ``xlCanFlushTransmitQueue`` of the XL driver, as it works only + for XL family devices. + + .. warning:: + Using this function will flush the queue and send a high voltage message (ID = 0, DLC = 0, no data). + """ + if self._can_protocol is CanProtocol.CAN_FD: + xl_can_tx_event = xlclass.XLcanTxEvent() + xl_can_tx_event.tag = xldefine.XL_CANFD_TX_EventTags.XL_CAN_EV_TAG_TX_MSG + xl_can_tx_event.tagData.canMsg.msgFlags |= ( + xldefine.XL_CANFD_TX_MessageFlags.XL_CAN_TXMSG_FLAG_HIGHPRIO + ) + + self.xldriver.xlCanTransmitEx( + self.port_handle, + self.mask, + ctypes.c_uint(1), + ctypes.c_uint(0), + xl_can_tx_event, + ) + else: + xl_event = xlclass.XLevent() + xl_event.tag = xldefine.XL_EventTags.XL_TRANSMIT_MSG + xl_event.tagData.msg.flags |= ( + xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_OVERRUN + | xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_WAKEUP + ) + + self.xldriver.xlCanTransmit( + self.port_handle, self.mask, ctypes.c_uint(1), xl_event + ) def shutdown(self) -> None: super().shutdown() diff --git a/test/test_vector.py b/test/test_vector.py index ab9f8a928..3db43fbbb 100644 --- a/test/test_vector.py +++ b/test/test_vector.py @@ -613,7 +613,38 @@ def test_receive_fd_non_msg_event() -> None: def test_flush_tx_buffer_mocked(mock_xldriver) -> None: bus = can.Bus(channel=0, interface="vector", _testing=True) bus.flush_tx_buffer() - can.interfaces.vector.canlib.xldriver.xlCanFlushTransmitQueue.assert_called() + transmit_args = can.interfaces.vector.canlib.xldriver.xlCanTransmit.call_args[0] + + num_msg = transmit_args[2] + assert num_msg.value == ctypes.c_uint(1).value + + event = transmit_args[3] + assert isinstance(event, xlclass.XLevent) + assert event.tag & xldefine.XL_EventTags.XL_TRANSMIT_MSG + assert event.tagData.msg.flags & ( + xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_OVERRUN + | xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_WAKEUP + ) + + +def test_flush_tx_buffer_fd_mocked(mock_xldriver) -> None: + bus = can.Bus(channel=0, interface="vector", fd=True, _testing=True) + bus.flush_tx_buffer() + transmit_args = can.interfaces.vector.canlib.xldriver.xlCanTransmitEx.call_args[0] + + num_msg = transmit_args[2] + assert num_msg.value == ctypes.c_uint(1).value + + num_msg_sent = transmit_args[3] + assert num_msg_sent.value == ctypes.c_uint(0).value + + event = transmit_args[4] + assert isinstance(event, xlclass.XLcanTxEvent) + assert event.tag & xldefine.XL_CANFD_TX_EventTags.XL_CAN_EV_TAG_TX_MSG + assert ( + event.tagData.canMsg.msgFlags + & xldefine.XL_CANFD_TX_MessageFlags.XL_CAN_TXMSG_FLAG_HIGHPRIO + ) @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") From b547dfc989f274a75a8ab7d2f3a25f57ff693e0a Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Mon, 9 Oct 2023 00:26:49 +0200 Subject: [PATCH 38/46] PCAN: remove Windows registry check (#1672) Co-authored-by: zariiii9003 --- can/interfaces/pcan/basic.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index a57340955..e003e83f9 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -24,9 +24,6 @@ IS_WINDOWS = PLATFORM == "Windows" IS_LINUX = PLATFORM == "Linux" -if IS_WINDOWS: - import winreg - logger = logging.getLogger("can.pcan") # /////////////////////////////////////////////////////////// @@ -668,14 +665,6 @@ class PCANBasic: def __init__(self): if platform.system() == "Windows": load_library_func = windll.LoadLibrary - - # look for Peak drivers in Windows registry - with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as reg: - try: - with winreg.OpenKey(reg, r"SOFTWARE\PEAK-System\PEAK-Drivers"): - pass - except OSError: - raise OSError("The PEAK-driver could not be found!") from None else: load_library_func = cdll.LoadLibrary From d2dc07d85a6630a083f3b38df833c6d535d53172 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Mon, 9 Oct 2023 00:36:23 +0200 Subject: [PATCH 39/46] Test Python 3.12 (#1673) Co-authored-by: zariiii9003 --- .github/workflows/ci.yml | 15 ++++++++++----- pyproject.toml | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 686a5d77b..c1aa8935a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,14 +22,16 @@ jobs: "3.9", "3.10", "3.11", + "3.12", "pypy-3.8", "pypy-3.9", ] - include: - # Only test on a single configuration while there are just pre-releases - - os: ubuntu-latest - experimental: true - python-version: "3.12.0-alpha - 3.12.0" + # uncomment when python 3.13.0 alpha is available + #include: + # # Only test on a single configuration while there are just pre-releases + # - os: ubuntu-latest + # experimental: true + # python-version: "3.13.0-alpha - 3.13.0" fail-fast: false steps: - uses: actions/checkout@v3 @@ -95,6 +97,9 @@ jobs: - name: mypy 3.11 run: | mypy --python-version 3.11 . + - name: mypy 3.12 + run: | + mypy --python-version 3.12 . - name: ruff run: | ruff check can diff --git a/pyproject.toml b/pyproject.toml index e20fd2785..a34508ee8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,6 @@ classifiers = [ "Intended Audience :: Telecommunications Industry", "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", "Natural Language :: English", - "Natural Language :: English", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", @@ -35,6 +34,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: System :: Hardware :: Hardware Drivers", From b0ba12fa833e60bc0661cb1e3f325501f6171609 Mon Sep 17 00:00:00 2001 From: Daniel Hrisca Date: Thu, 12 Oct 2023 21:15:04 +0300 Subject: [PATCH 40/46] Vector: Skip the can_op_mode check if the device reports can_op_mode=0 (#1678) --- can/interfaces/vector/canlib.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index bc564958f..024f6a4c9 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -538,16 +538,19 @@ def _check_can_settings( ) # check CAN operation mode - if fd: - settings_acceptable &= bool( - bus_params_data.can_op_mode - & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CANFD - ) - elif bus_params_data.can_op_mode != 0: # can_op_mode is always 0 for cancaseXL - settings_acceptable &= bool( - bus_params_data.can_op_mode - & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CAN20 - ) + # skip the check if can_op_mode is 0 + # as it happens for cancaseXL, VN7600 and sometimes on other hardware (VN1640) + if bus_params_data.can_op_mode: + if fd: + settings_acceptable &= bool( + bus_params_data.can_op_mode + & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CANFD + ) + else: + settings_acceptable &= bool( + bus_params_data.can_op_mode + & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CAN20 + ) # check bitrates if bitrate: From b2689ae184363692fc030248e8b55fd0e40ae068 Mon Sep 17 00:00:00 2001 From: Daniel Hrisca Date: Thu, 12 Oct 2023 21:18:20 +0300 Subject: [PATCH 41/46] Add BitTiming.iterate_from_sample_point static methods (#1671) * add the possibility to return all the possible solutions using the from_sample_point static methods * Format code with black * add iterators for timings from sample point * update docstrings * make ruff happy * add a small test for iterate_from_sample_point * Format code with black --------- Co-authored-by: danielhrisca --- can/bit_timing.py | 114 +++++++++++++++++++++++++++++++--------- test/test_bit_timing.py | 26 +++++++++ 2 files changed, 115 insertions(+), 25 deletions(-) diff --git a/can/bit_timing.py b/can/bit_timing.py index 3e8cb1bcd..f5d50eac1 100644 --- a/can/bit_timing.py +++ b/can/bit_timing.py @@ -213,17 +213,10 @@ def from_registers( ) @classmethod - def from_sample_point( + def iterate_from_sample_point( cls, f_clock: int, bitrate: int, sample_point: float = 69.0 - ) -> "BitTiming": - """Create a :class:`~can.BitTiming` instance for a sample point. - - This function tries to find bit timings, which are close to the requested - sample point. It does not take physical bus properties into account, so the - calculated bus timings might not work properly for you. - - The :func:`oscillator_tolerance` function might be helpful to evaluate the - bus timings. + ) -> Iterator["BitTiming"]: + """Create a :class:`~can.BitTiming` iterator with all the solutions for a sample point. :param int f_clock: The CAN system clock frequency in Hz. @@ -238,7 +231,6 @@ def from_sample_point( if sample_point < 50.0: raise ValueError(f"sample_point (={sample_point}) must not be below 50%.") - possible_solutions: List[BitTiming] = [] for brp in range(1, 65): nbt = round(int(f_clock / (bitrate * brp))) if nbt < 8: @@ -264,10 +256,40 @@ def from_sample_point( sjw=sjw, strict=True, ) - possible_solutions.append(bt) + yield bt except ValueError: continue + @classmethod + def from_sample_point( + cls, f_clock: int, bitrate: int, sample_point: float = 69.0 + ) -> "BitTiming": + """Create a :class:`~can.BitTiming` instance for a sample point. + + This function tries to find bit timings, which are close to the requested + sample point. It does not take physical bus properties into account, so the + calculated bus timings might not work properly for you. + + The :func:`oscillator_tolerance` function might be helpful to evaluate the + bus timings. + + :param int f_clock: + The CAN system clock frequency in Hz. + :param int bitrate: + Bitrate in bit/s. + :param int sample_point: + The sample point value in percent. + :raises ValueError: + if the arguments are invalid. + """ + + if sample_point < 50.0: + raise ValueError(f"sample_point (={sample_point}) must not be below 50%.") + + possible_solutions: List[BitTiming] = list( + cls.iterate_from_sample_point(f_clock, bitrate, sample_point) + ) + if not possible_solutions: raise ValueError("No suitable bit timings found.") @@ -729,22 +751,15 @@ def from_bitrate_and_segments( # pylint: disable=too-many-arguments return bt @classmethod - def from_sample_point( + def iterate_from_sample_point( cls, f_clock: int, nom_bitrate: int, nom_sample_point: float, data_bitrate: int, data_sample_point: float, - ) -> "BitTimingFd": - """Create a :class:`~can.BitTimingFd` instance for a given nominal/data sample point pair. - - This function tries to find bit timings, which are close to the requested - sample points. It does not take physical bus properties into account, so the - calculated bus timings might not work properly for you. - - The :func:`oscillator_tolerance` function might be helpful to evaluate the - bus timings. + ) -> Iterator["BitTimingFd"]: + """Create an :class:`~can.BitTimingFd` iterator with all the solutions for a sample point. :param int f_clock: The CAN system clock frequency in Hz. @@ -769,8 +784,6 @@ def from_sample_point( f"data_sample_point (={data_sample_point}) must not be below 50%." ) - possible_solutions: List[BitTimingFd] = [] - sync_seg = 1 for nom_brp in range(1, 257): @@ -818,10 +831,61 @@ def from_sample_point( data_sjw=data_sjw, strict=True, ) - possible_solutions.append(bt) + yield bt except ValueError: continue + @classmethod + def from_sample_point( + cls, + f_clock: int, + nom_bitrate: int, + nom_sample_point: float, + data_bitrate: int, + data_sample_point: float, + ) -> "BitTimingFd": + """Create a :class:`~can.BitTimingFd` instance for a sample point. + + This function tries to find bit timings, which are close to the requested + sample points. It does not take physical bus properties into account, so the + calculated bus timings might not work properly for you. + + The :func:`oscillator_tolerance` function might be helpful to evaluate the + bus timings. + + :param int f_clock: + The CAN system clock frequency in Hz. + :param int nom_bitrate: + Nominal bitrate in bit/s. + :param int nom_sample_point: + The sample point value of the arbitration phase in percent. + :param int data_bitrate: + Data bitrate in bit/s. + :param int data_sample_point: + The sample point value of the data phase in percent. + :raises ValueError: + if the arguments are invalid. + """ + if nom_sample_point < 50.0: + raise ValueError( + f"nom_sample_point (={nom_sample_point}) must not be below 50%." + ) + + if data_sample_point < 50.0: + raise ValueError( + f"data_sample_point (={data_sample_point}) must not be below 50%." + ) + + possible_solutions: List[BitTimingFd] = list( + cls.iterate_from_sample_point( + f_clock, + nom_bitrate, + nom_sample_point, + data_bitrate, + data_sample_point, + ) + ) + if not possible_solutions: raise ValueError("No suitable bit timings found.") diff --git a/test/test_bit_timing.py b/test/test_bit_timing.py index 6852687a5..514c31244 100644 --- a/test/test_bit_timing.py +++ b/test/test_bit_timing.py @@ -286,6 +286,32 @@ def test_from_sample_point(): ) +def test_iterate_from_sample_point(): + for sp in range(50, 100): + solutions = list( + can.BitTiming.iterate_from_sample_point( + f_clock=16_000_000, + bitrate=500_000, + sample_point=sp, + ) + ) + assert len(solutions) >= 2 + + for nsp in range(50, 100): + for dsp in range(50, 100): + solutions = list( + can.BitTimingFd.iterate_from_sample_point( + f_clock=80_000_000, + nom_bitrate=500_000, + nom_sample_point=nsp, + data_bitrate=2_000_000, + data_sample_point=dsp, + ) + ) + + assert len(solutions) >= 2 + + def test_equality(): t1 = can.BitTiming.from_registers(f_clock=8_000_000, btr0=0x00, btr1=0x14) t2 = can.BitTiming(f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1, nof_samples=1) From e869fb7242f8a4bf12593f6a37550a462764ca00 Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Mon, 16 Oct 2023 15:47:41 -0400 Subject: [PATCH 42/46] Fix ThreadBasedCyclicSendTask thread not being stopped on Windows (#1679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also adding unit test to cover RestartableCyclicTaskABC Co-authored-by: Pierre-Luc Tessier Gagné --- can/broadcastmanager.py | 5 +++-- test/simplecyclic_test.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 398114a59..0ac9b6adc 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -269,9 +269,10 @@ def __init__( self.start() def stop(self) -> None: - if USE_WINDOWS_EVENTS: - win32event.CancelWaitableTimer(self.event.handle) self.stopped = True + if USE_WINDOWS_EVENTS: + # Reset and signal any pending wait by setting the timer to 0 + win32event.SetWaitableTimer(self.event.handle, 0, 0, None, None, False) def start(self) -> None: self.stopped = False diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 650a1fddf..21e88e9f0 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -152,6 +152,41 @@ def test_stopping_perodic_tasks(self): bus.shutdown() + def test_restart_perodic_tasks(self): + period = 0.01 + safe_timeout = period * 5 + + msg = can.Message( + is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7] + ) + + with can.ThreadSafeBus(interface="virtual", receive_own_messages=True) as bus: + task = bus.send_periodic(msg, period) + self.assertIsInstance(task, can.broadcastmanager.RestartableCyclicTaskABC) + + # Test that the task is sending messages + sleep(safe_timeout) + assert not bus.queue.empty(), "messages should have been transmitted" + + # Stop the task and check that messages are no longer being sent + bus.stop_all_periodic_tasks(remove_tasks=False) + sleep(safe_timeout) + while not bus.queue.empty(): + bus.recv(timeout=period) + sleep(safe_timeout) + assert bus.queue.empty(), "messages should not have been transmitted" + + # Restart the task and check that messages are being sent again + task.start() + sleep(safe_timeout) + assert not bus.queue.empty(), "messages should have been transmitted" + + # Stop all tasks and wait for the thread to exit + bus.stop_all_periodic_tasks() + if isinstance(task, can.broadcastmanager.ThreadBasedCyclicSendTask): + # Avoids issues where the thread is still running when the bus is shutdown + task.thread.join(safe_timeout) + @unittest.skipIf(IS_CI, "fails randomly when run on CI server") def test_thread_based_cyclic_send_task(self): bus = can.ThreadSafeBus(interface="virtual") From 7b353ca240ea2ff73901eabce3996861e17a2deb Mon Sep 17 00:00:00 2001 From: MattWoodhead Date: Mon, 16 Oct 2023 22:12:52 +0100 Subject: [PATCH 43/46] Implement _detect_available_configs for the Ixxat bus. (#1607) * Add _detect_available_configs to ixxat bus * Add typing and cover CI test failure * Format code with black * Format code with black * re-order imports for ruff * Update ixxat docs * fix doctest * Update test_interface_ixxat.py * make ruff happy --------- Co-authored-by: MattWoodhead Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> --- can/interfaces/ixxat/canlib.py | 7 +++- can/interfaces/ixxat/canlib_vcinpl.py | 55 +++++++++++++++++++++++++- can/interfaces/ixxat/canlib_vcinpl2.py | 10 ++--- can/interfaces/pcan/pcan.py | 4 +- doc/interfaces/ixxat.rst | 31 +++++++++++++-- test/test_interface_ixxat.py | 12 ++++++ 6 files changed, 105 insertions(+), 14 deletions(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index f18c86acd..330ccdcd9 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -1,4 +1,4 @@ -from typing import Callable, Optional, Sequence, Union +from typing import Callable, List, Optional, Sequence, Union import can.interfaces.ixxat.canlib_vcinpl as vcinpl import can.interfaces.ixxat.canlib_vcinpl2 as vcinpl2 @@ -8,6 +8,7 @@ CyclicSendTaskABC, Message, ) +from can.typechecking import AutoDetectedConfig class IXXATBus(BusABC): @@ -170,3 +171,7 @@ def state(self) -> BusState: Return the current state of the hardware """ return self.bus.state + + @staticmethod + def _detect_available_configs() -> List[AutoDetectedConfig]: + return vcinpl._detect_available_configs() diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py index 922c683b8..334adee11 100644 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ b/can/interfaces/ixxat/canlib_vcinpl.py @@ -14,7 +14,7 @@ import logging import sys import warnings -from typing import Callable, Optional, Sequence, Tuple, Union +from typing import Callable, List, Optional, Sequence, Tuple, Union from can import ( BusABC, @@ -28,6 +28,7 @@ from can.ctypesutil import HANDLE, PHANDLE, CLibrary from can.ctypesutil import HRESULT as ctypes_HRESULT from can.exceptions import CanInitializationError, CanInterfaceNotImplementedError +from can.typechecking import AutoDetectedConfig from can.util import deprecated_args_alias from . import constants, structures @@ -943,3 +944,55 @@ def get_ixxat_hwids(): _canlib.vciEnumDeviceClose(device_handle) return hwids + + +def _detect_available_configs() -> List[AutoDetectedConfig]: + config_list = [] # list in wich to store the resulting bus kwargs + + # used to detect HWID + device_handle = HANDLE() + device_info = structures.VCIDEVICEINFO() + + # used to attempt to open channels + channel_handle = HANDLE() + device_handle2 = HANDLE() + + try: + _canlib.vciEnumDeviceOpen(ctypes.byref(device_handle)) + while True: + try: + _canlib.vciEnumDeviceNext(device_handle, ctypes.byref(device_info)) + except StopIteration: + break + else: + hwid = device_info.UniqueHardwareId.AsChar.decode("ascii") + _canlib.vciDeviceOpen( + ctypes.byref(device_info.VciObjectId), + ctypes.byref(device_handle2), + ) + for channel in range(4): + try: + _canlib.canChannelOpen( + device_handle2, + channel, + constants.FALSE, + ctypes.byref(channel_handle), + ) + except Exception: + # Array outside of bounds error == accessing a channel not in the hardware + break + else: + _canlib.canChannelClose(channel_handle) + config_list.append( + { + "interface": "ixxat", + "channel": channel, + "unique_hardware_id": hwid, + } + ) + _canlib.vciDeviceClose(device_handle2) + _canlib.vciEnumDeviceClose(device_handle) + except AttributeError: + pass # _canlib is None in the CI tests -> return a blank list + + return config_list diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index 2c306c880..aaefa1bf9 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -509,17 +509,15 @@ def __init__( tseg1_abr is None or tseg2_abr is None or sjw_abr is None ): raise ValueError( - "To use bitrate {} (that has not predefined preset) is mandatory to use also parameters tseg1_abr, tseg2_abr and swj_abr".format( - bitrate - ) + f"To use bitrate {bitrate} (that has not predefined preset) is mandatory " + f"to use also parameters tseg1_abr, tseg2_abr and swj_abr" ) if data_bitrate not in constants.CAN_DATABITRATE_PRESETS and ( tseg1_dbr is None or tseg2_dbr is None or sjw_dbr is None ): raise ValueError( - "To use data_bitrate {} (that has not predefined preset) is mandatory to use also parameters tseg1_dbr, tseg2_dbr and swj_dbr".format( - data_bitrate - ) + f"To use data_bitrate {data_bitrate} (that has not predefined preset) is mandatory " + f"to use also parameters tseg1_dbr, tseg2_dbr and swj_dbr" ) if rx_fifo_size <= 0: diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 01a4b1dc3..884c1680b 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -396,9 +396,7 @@ def bits(n): for b in bits(error): stsReturn = self.m_objPCANBasic.GetErrorText(b, 0x9) if stsReturn[0] != PCAN_ERROR_OK: - text = "An error occurred. Error-code's text ({:X}h) couldn't be retrieved".format( - error - ) + text = f"An error occurred. Error-code's text ({error:X}h) couldn't be retrieved" else: text = stsReturn[1].decode("utf-8", errors="replace") diff --git a/doc/interfaces/ixxat.rst b/doc/interfaces/ixxat.rst index f73a01036..1337bf738 100644 --- a/doc/interfaces/ixxat.rst +++ b/doc/interfaces/ixxat.rst @@ -56,17 +56,42 @@ VCI documentation, section "Message filters" for more info. List available devices ---------------------- -In case you have connected multiple IXXAT devices, you have to select them by using their unique hardware id. -To get a list of all connected IXXAT you can use the function ``get_ixxat_hwids()`` as demonstrated below: + +In case you have connected multiple IXXAT devices, you have to select them by using their unique hardware id. +The function :meth:`~can.detect_available_configs` can be used to generate a list of :class:`~can.BusABC` constructors +(including the channel number and unique hardware ID number for the connected devices). .. testsetup:: ixxat + from unittest.mock import Mock + import can + assert hasattr(can, "detect_available_configs") + can.detect_available_configs = Mock( + "interface", + return_value=[{'interface': 'ixxat', 'channel': 0, 'unique_hardware_id': 'HW441489'}, {'interface': 'ixxat', 'channel': 0, 'unique_hardware_id': 'HW107422'}, {'interface': 'ixxat', 'channel': 1, 'unique_hardware_id': 'HW107422'}], + ) + + .. doctest:: ixxat + + >>> import can + >>> configs = can.detect_available_configs("ixxat") + >>> for config in configs: + ... print(config) + {'interface': 'ixxat', 'channel': 0, 'unique_hardware_id': 'HW441489'} + {'interface': 'ixxat', 'channel': 0, 'unique_hardware_id': 'HW107422'} + {'interface': 'ixxat', 'channel': 1, 'unique_hardware_id': 'HW107422'} + + +You may also get a list of all connected IXXAT devices using the function ``get_ixxat_hwids()`` as demonstrated below: + + .. testsetup:: ixxat2 + from unittest.mock import Mock import can.interfaces.ixxat assert hasattr(can.interfaces.ixxat, "get_ixxat_hwids") can.interfaces.ixxat.get_ixxat_hwids = Mock(side_effect=lambda: ['HW441489', 'HW107422']) - .. doctest:: ixxat + .. doctest:: ixxat2 >>> from can.interfaces.ixxat import get_ixxat_hwids >>> for hwid in get_ixxat_hwids(): diff --git a/test/test_interface_ixxat.py b/test/test_interface_ixxat.py index 2ff016d97..90b5f7adc 100644 --- a/test/test_interface_ixxat.py +++ b/test/test_interface_ixxat.py @@ -51,6 +51,18 @@ def setUp(self): raise unittest.SkipTest("not available on this platform") def test_bus_creation(self): + try: + configs = can.detect_available_configs("ixxat") + if configs: + for interface_kwargs in configs: + bus = can.Bus(**interface_kwargs) + bus.shutdown() + else: + raise unittest.SkipTest("No adapters were detected") + except can.CanInterfaceNotImplementedError: + raise unittest.SkipTest("not available on this platform") + + def test_bus_creation_incorrect_channel(self): # non-existent channel -> use arbitrary high value with self.assertRaises(can.CanInitializationError): can.Bus(interface="ixxat", channel=0xFFFF) From 2d609005b2b51391638b86fcb802544411c5e4cc Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Mon, 16 Oct 2023 23:14:39 +0200 Subject: [PATCH 44/46] Add BitTiming/BitTimingFd support to KvaserBus (#1510) * add BitTiming parameter to KvaserBus * implement tests for bittiming classes with kvaser * set default number of samples to 1 * undo last change --- can/interfaces/kvaser/canlib.py | 86 +++++++++++++++++++++++++-------- test/test_kvaser.py | 32 ++++++++++++ 2 files changed, 98 insertions(+), 20 deletions(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 38949137d..0983e28dc 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -10,11 +10,13 @@ import logging import sys import time +from typing import Optional, Union -from can import BusABC, CanProtocol, Message -from can.util import time_perfcounter_correlation +from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message +from can.exceptions import CanError, CanInitializationError, CanOperationError +from can.typechecking import CanFilters +from can.util import check_or_adjust_timing_clock, time_perfcounter_correlation -from ...exceptions import CanError, CanInitializationError, CanOperationError from . import constants as canstat from . import structures @@ -199,6 +201,17 @@ def __check_bus_handle_validity(handle, function, arguments): errcheck=__check_status_initialization, ) + canSetBusParamsC200 = __get_canlib_function( + "canSetBusParamsC200", + argtypes=[ + c_canHandle, + ctypes.c_byte, + ctypes.c_byte, + ], + restype=canstat.c_canStatus, + errcheck=__check_status_initialization, + ) + canSetBusParamsFd = __get_canlib_function( "canSetBusParamsFd", argtypes=[ @@ -360,7 +373,13 @@ class KvaserBus(BusABC): The CAN Bus implemented for the Kvaser interface. """ - def __init__(self, channel, can_filters=None, **kwargs): + def __init__( + self, + channel: int, + can_filters: Optional[CanFilters] = None, + timing: Optional[Union[BitTiming, BitTimingFd]] = None, + **kwargs, + ): """ :param int channel: The Channel id to create this bus with. @@ -370,6 +389,12 @@ def __init__(self, channel, can_filters=None, **kwargs): Backend Configuration + :param timing: + An instance of :class:`~can.BitTiming` or :class:`~can.BitTimingFd` + to specify the bit timing parameters for the Kvaser interface. If provided, it + takes precedence over the all other timing-related parameters. + Note that the `f_clock` property of the `timing` instance must be 16_000_000 (16MHz) + for standard CAN or 80_000_000 (80MHz) for CAN FD. :param int bitrate: Bitrate of channel in bit/s :param bool accept_virtual: @@ -427,7 +452,7 @@ def __init__(self, channel, can_filters=None, **kwargs): exclusive = kwargs.get("exclusive", False) override_exclusive = kwargs.get("override_exclusive", False) accept_virtual = kwargs.get("accept_virtual", True) - fd = kwargs.get("fd", False) + fd = isinstance(timing, BitTimingFd) if timing else kwargs.get("fd", False) data_bitrate = kwargs.get("data_bitrate", None) try: @@ -468,22 +493,43 @@ def __init__(self, channel, can_filters=None, **kwargs): ctypes.byref(ctypes.c_long(TIMESTAMP_RESOLUTION)), 4, ) - - if fd: - if "tseg1" not in kwargs 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, data_bitrate, tseg1, tseg2, sjw) + if isinstance(timing, BitTimingFd): + timing = check_or_adjust_timing_clock(timing, [80_000_000]) + canSetBusParams( + self._read_handle, + timing.nom_bitrate, + timing.nom_tseg1, + timing.nom_tseg2, + timing.nom_sjw, + 1, + 0, + ) + canSetBusParamsFd( + self._read_handle, + timing.data_bitrate, + timing.data_tseg1, + timing.data_tseg2, + timing.data_sjw, + ) + elif isinstance(timing, BitTiming): + timing = check_or_adjust_timing_clock(timing, [16_000_000]) + canSetBusParamsC200(self._read_handle, timing.btr0, timing.btr1) else: - if "tseg1" not in kwargs and bitrate in BITRATE_OBJS: - bitrate = BITRATE_OBJS[bitrate] - canSetBusParams(self._read_handle, bitrate, tseg1, tseg2, sjw, no_samp, 0) + if fd: + if "tseg1" not in kwargs 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, data_bitrate, tseg1, tseg2, sjw) + else: + if "tseg1" not in kwargs 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) local_echo = single_handle or receive_own_messages diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 1254f2fc7..abaf7b38f 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -21,6 +21,7 @@ def setUp(self): canlib.canIoCtl = Mock(return_value=0) canlib.canIoCtlInit = Mock(return_value=0) canlib.kvReadTimer = Mock() + canlib.canSetBusParamsC200 = Mock() canlib.canSetBusParams = Mock() canlib.canSetBusParamsFd = Mock() canlib.canBusOn = Mock() @@ -179,6 +180,37 @@ def test_canfd_default_data_bitrate(self): 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0 ) + def test_can_timing(self): + canlib.canSetBusParams.reset_mock() + canlib.canSetBusParamsFd.reset_mock() + timing = can.BitTiming.from_bitrate_and_segments( + f_clock=16_000_000, + bitrate=125_000, + tseg1=13, + tseg2=2, + sjw=1, + ) + can.Bus(channel=0, interface="kvaser", timing=timing) + canlib.canSetBusParamsC200.assert_called_once_with(0, timing.btr0, timing.btr1) + + def test_canfd_timing(self): + canlib.canSetBusParams.reset_mock() + canlib.canSetBusParamsFd.reset_mock() + timing = can.BitTimingFd.from_bitrate_and_segments( + f_clock=80_000_000, + nom_bitrate=500_000, + nom_tseg1=68, + nom_tseg2=11, + nom_sjw=10, + data_bitrate=2_000_000, + data_tseg1=10, + data_tseg2=9, + data_sjw=8, + ) + can.Bus(channel=0, interface="kvaser", timing=timing) + canlib.canSetBusParams.assert_called_once_with(0, 500_000, 68, 11, 10, 1, 0) + canlib.canSetBusParamsFd.assert_called_once_with(0, 2_000_000, 10, 9, 8) + def test_canfd_nondefault_data_bitrate(self): canlib.canSetBusParams.reset_mock() canlib.canSetBusParamsFd.reset_mock() From 61ee42b2ae61c882f40033b97773b59c37acac59 Mon Sep 17 00:00:00 2001 From: Lukas Magel Date: Tue, 17 Oct 2023 11:47:20 +0200 Subject: [PATCH 45/46] Update Changelog for Release v.4.3.0rc0 (#1680) * Upate Changelog for Release v.4.3.0 * Update CHANGELOG.md Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> * Update CHANGELOG.md Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> * Update CHANGELOG.md Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> * Update CHANGELOG.md Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> * Update CHANGELOG.md Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> * Update CHANGELOG.md Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> * Make requested changes to CHANGELOG, sort by PR ID * Fix remaining comments * Add PR for Kvaser BitTiming support * add 1679 (bugfix for 1666) * Add PR #1607, change changelog version to 4.3.0rc * Bump project version to 4.3.0rc0 --------- Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> --- CHANGELOG.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ can/__init__.py | 2 +- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 493ac1966..651b222fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,58 @@ +Version 4.3.0rc0 +=========== + +Breaking Changes +---------------- +* Raise Minimum Python Version to 3.8 (#1597) +* Do not stop notifier if exception was handled (#1645) + +Bug Fixes +--------- +* Vector: channel detection fails, if there is an active flexray channel (#1634) +* ixxat: Fix exception in 'state' property on bus coupling errors (#1647) +* NeoVi: Fixed serial number range (#1650) +* PCAN: Fix timestamp offset due to timezone (#1651) +* Catch `pywintypes.error` in broadcast manager (#1659) +* Fix BLFReader error for incomplete or truncated stream (#1662) +* PCAN: remove Windows registry check to fix 32bit compatibility (#1672) +* Vector: Skip the `can_op_mode check` if the device reports `can_op_mode=0` (#1678) + +Features +-------- + +### API +* Add `modifier_callback` parameter to `BusABC.send_periodic` for auto-modifying cyclic tasks (#703) +* Add `protocol` property to BusABC to determine active CAN Protocol (#1532) +* Change Bus constructor implementation and typing (#1557) +* Add optional `strict` parameter to relax BitTiming & BitTimingFd Validation (#1618) +* Add `BitTiming.iterate_from_sample_point` static methods (#1671) + +### IO +* Can Player compatibility with interfaces that use additional configuration (#1610) + +### Interface Improvements +* Kvaser: Add BitTiming/BitTimingFd support to KvaserBus (#1510) +* Ixxat: Implement `detect_available_configs` for the Ixxat bus. (#1607) +* NeoVi: Enable send and receive on network ID above 255 (#1627) +* Vector: Send HighPriority Message to flush Tx buffer (#1636) +* PCAN: Optimize send performance (#1640) +* PCAN: Support version string of older PCAN basic API (#1644) +* Kvaser: add parameter exclusive and `override_exclusive` (#1660) + +### Miscellaneous +* Distinguish Text/Binary-IO for Reader/Writer classes. (#1585) +* Convert setup.py to pyproject.toml (#1592) +* activate ruff pycodestyle checks (#1602) +* Update linter instructions in development.rst (#1603) +* remove unnecessary script files (#1604) +* BigEndian test fixes (#1625) +* align `ID:` in can.Message string (#1635) +* Use same configuration file as Linux on macOS (#1657) +* We do not need to account for drift when we `USE_WINDOWS_EVENTS` (#1666, #1679) +* Update linters, activate more ruff rules (#1669) +* Add Python 3.12 Support / Test Python 3.12 (#1673) + + Version 4.2.2 ============= diff --git a/can/__init__.py b/can/__init__.py index 46a461b38..48dda308d 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging from typing import Any, Dict -__version__ = "4.2.2" +__version__ = "4.3.0rc0" __all__ = [ "ASCReader", "ASCWriter", From 38c4dc4b9ff2a932cdd1b776fb6a62df8a3e63b6 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:55:23 +0200 Subject: [PATCH 46/46] Vector: use global channel_index if provided (#1681) * fix XL_ERR_INVALID_CHANNEL_MASK for multiple devices with the same serial * add CHANGELOG.md entry --- CHANGELOG.md | 1 + can/interfaces/vector/canlib.py | 21 ++++++++++++----- test/test_vector.py | 41 +++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 651b222fb..0220b11a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Bug Fixes * Fix BLFReader error for incomplete or truncated stream (#1662) * PCAN: remove Windows registry check to fix 32bit compatibility (#1672) * Vector: Skip the `can_op_mode check` if the device reports `can_op_mode=0` (#1678) +* Vector: using the config from `detect_available_configs` might raise XL_ERR_INVALID_CHANNEL_MASK error (#1681) Features -------- diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 024f6a4c9..576fa26cf 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -202,12 +202,20 @@ def __init__( self._can_protocol = CanProtocol.CAN_FD if is_fd else CanProtocol.CAN_20 for channel in self.channels: - channel_index = self._find_global_channel_idx( - channel=channel, - serial=serial, - app_name=app_name, - channel_configs=channel_configs, - ) + if (_channel_index := kwargs.get("channel_index", None)) is not None: + # VectorBus._detect_available_configs() might return multiple + # devices with the same serial number, e.g. if a VN8900 is connected via both USB and Ethernet + # at the same time. If the VectorBus is instantiated with a config, that was returned from + # VectorBus._detect_available_configs(), then use the contained global channel_index + # to avoid any ambiguities. + channel_index = cast(int, _channel_index) + else: + channel_index = self._find_global_channel_idx( + channel=channel, + serial=serial, + app_name=app_name, + channel_configs=channel_configs, + ) LOG.debug("Channel index %d found", channel) channel_mask = 1 << channel_index @@ -950,6 +958,7 @@ def _detect_available_configs() -> List[AutoDetectedConfig]: "interface": "vector", "channel": channel_config.hw_channel, "serial": channel_config.serial_number, + "channel_index": channel_config.channel_index, # data for use in VectorBus.set_application_config(): "hw_type": channel_config.hw_type, "hw_index": channel_config.hw_index, diff --git a/test/test_vector.py b/test/test_vector.py index 3db43fbbb..3e53bdaff 100644 --- a/test/test_vector.py +++ b/test/test_vector.py @@ -118,6 +118,22 @@ def test_bus_creation() -> None: bus.shutdown() +@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") +def test_bus_creation_channel_index() -> None: + channel_index = 3 + bus = can.Bus( + channel=0, + serial=_find_virtual_can_serial(), + channel_index=channel_index, + interface="vector", + ) + assert isinstance(bus, canlib.VectorBus) + assert bus.protocol == can.CanProtocol.CAN_20 + assert bus.channel_masks[0] == 1 << channel_index + + bus.shutdown() + + def test_bus_creation_bitrate_mocked(mock_xldriver) -> None: bus = can.Bus(channel=0, interface="vector", bitrate=200_000, _testing=True) assert isinstance(bus, canlib.VectorBus) @@ -833,6 +849,31 @@ def test_get_channel_configs() -> None: canlib._get_xl_driver_config = _original_func +@pytest.mark.skipif( + sys.byteorder != "little", reason="Test relies on little endian data." +) +def test_detect_available_configs() -> None: + _original_func = canlib._get_xl_driver_config + canlib._get_xl_driver_config = _get_predefined_xl_driver_config + + available_configs = canlib.VectorBus._detect_available_configs() + + assert len(available_configs) == 5 + + assert available_configs[0]["interface"] == "vector" + assert available_configs[0]["channel"] == 2 + assert available_configs[0]["serial"] == 1001 + assert available_configs[0]["channel_index"] == 2 + assert available_configs[0]["hw_type"] == xldefine.XL_HardwareType.XL_HWTYPE_VN8900 + assert available_configs[0]["hw_index"] == 0 + assert available_configs[0]["supports_fd"] is True + assert isinstance( + available_configs[0]["vector_channel_config"], VectorChannelConfig + ) + + canlib._get_xl_driver_config = _original_func + + @pytest.mark.skipif(not IS_WINDOWS, reason="Windows specific test") def test_winapi_availability() -> None: assert canlib.WaitForSingleObject is not None